SDK quickstart: Kotlin (Android)
Generate a typed Kotlin client for the /v1 Developer API from its OpenAPI spec
and make your first call.
No hand-written Kotlin SDK is published yet — you generate one from the live spec. An official SDK is a roadmap item.
Which credential? A shared API key (
sk_live_…) suits your own automation and server-side jobs. A first-party Android app a user logs into should authenticate as that user with OAuth 2.0 + PKCE and send the user's token — see Build a client. The steps below use an API key; swap in the user's bearer token for a logged-in app.
1. Get the spec
https://api-dev.traxstreaming.live/v1/openapi.json
2. Generate a typed client
Use openapi-generator with the
kotlin generator (Retrofit2 + coroutines is a good default for Android):
npm i -g @openapitools/openapi-generator-cli
openapi-generator-cli generate \
-i https://api-dev.traxstreaming.live/v1/openapi.json \
-g kotlin \
--additional-properties=library=jvm-retrofit2,useCoroutines=true,packageName=live.traxstreaming.api \
-o ./trax-client-kotlin
Add the generated module to your Gradle build (include(":trax-client-kotlin")
in settings.gradle, then depend on it from your app module).
3. Authenticate
Building an app a user logs into? Sign them in with OAuth 2.0 + PKCE and send their token — jump to Sign the user in (OIDC + PKCE) below.
Automating your own account? Create an API key from your account's
Developer / API Keys screen and copy the sk_live_… secret once. Store it
with EncryptedSharedPreferences or the Android Keystore — never hardcoded
in the APK or in strings.xml.
Sign the user in (OIDC + PKCE)
A first-party Android app authenticates the user against the TRaX identity
provider with the Authorization-Code flow + PKCE, then calls /v1 with the
user's access token. The user signs in (password, MFA / passkey) on the IdP's
hosted login page in a Custom Tab — never in your UI. See
Authentication → User login for mobile & native
apps
for the concept; this is the concrete Android.
Use AppAuth-Android — the OpenID Foundation's certified client. It drives the Custom Tab, generates the PKCE pair, and does the token exchange + refresh for you.
// build.gradle.kts
implementation("net.openid:appauth:0.11.1")
implementation("androidx.security:security-crypto:1.1.0-alpha06") // EncryptedSharedPreferences
Config (dev shown; swap the issuer + CLIENT_ID for production):
object TraxAuth {
// Dev IdP. Production: https://auth.traxstreaming.live
const val ISSUER = "https://auth-dev.traxstreaming.live"
const val AUTHORIZE = "$ISSUER/oauth/v2/authorize"
const val TOKEN = "$ISSUER/oauth/v2/token"
const val END_SESSION = "$ISSUER/oidc/v1/end_session"
// Public native client (no secret). Dev value; production has its own id.
const val CLIENT_ID = "385862192278274086"
// Must match a redirect URI registered on the client, exactly.
const val REDIRECT_URI = "live.traxstreaming.android://oauth/callback"
const val SCOPE = "openid profile email offline_access"
const val API_BASE = "https://api-dev.traxstreaming.live"
}
Register the redirect scheme so the Custom Tab can hand the code back — either via AppAuth's manifest placeholder, or a verified App Link:
// build.gradle.kts — app module
android {
defaultConfig {
manifestPlaceholders["appAuthRedirectScheme"] = "live.traxstreaming.android"
}
}
The login round-trip — launch the hosted page, then exchange the returned
code. AppAuth generates and tracks the PKCE code_verifier/code_challenge
internally, so you never handle them by hand:
import android.content.Intent
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
import net.openid.appauth.*
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
class LoginActivity : AppCompatActivity() {
private val authService by lazy { AuthorizationService(this) }
private val serviceConfig = AuthorizationServiceConfiguration(
Uri.parse(TraxAuth.AUTHORIZE),
Uri.parse(TraxAuth.TOKEN),
/* registrationEndpoint = */ null,
Uri.parse(TraxAuth.END_SESSION),
)
// Registered launcher that receives the Custom Tab result.
private val authLauncher =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
val resp = AuthorizationResponse.fromIntent(result.data!!)
val ex = AuthorizationException.fromIntent(result.data)
when {
resp != null -> exchange(resp) // got ?code — swap for tokens
else -> onLoginFailed(ex)
}
}
fun startLogin() {
val request = AuthorizationRequest.Builder(
serviceConfig,
TraxAuth.CLIENT_ID,
ResponseTypeValues.CODE,
Uri.parse(TraxAuth.REDIRECT_URI),
)
.setScope(TraxAuth.SCOPE) // includes offline_access
.build() // PKCE S256 added automatically
authLauncher.launch(authService.getAuthorizationRequestIntent(request))
}
private fun exchange(resp: AuthorizationResponse) {
// resp.createTokenExchangeRequest() carries the code_verifier for PKCE.
authService.performTokenRequest(resp.createTokenExchangeRequest()) { tokens, ex ->
if (tokens != null) {
TokenStore(this).save(tokens.accessToken!!, tokens.refreshToken)
onLoggedIn(tokens.accessToken!!)
} else {
onLoginFailed(ex)
}
}
}
override fun onDestroy() {
authService.dispose()
super.onDestroy()
}
}
Silent refresh — before the access token expires, mint a new one with the refresh token (no Custom Tab), as a coroutine:
suspend fun refresh(authService: AuthorizationService, refreshToken: String): String =
suspendCoroutine { cont ->
val request = TokenRequest.Builder(serviceConfig, TraxAuth.CLIENT_ID)
.setGrantType(GrantTypeValues.REFRESH_TOKEN)
.setRefreshToken(refreshToken)
.build()
authService.performTokenRequest(request) { tokens, ex ->
if (tokens?.accessToken != null) cont.resume(tokens.accessToken!!)
else cont.resumeWith(Result.failure(ex ?: RuntimeException("refresh failed")))
}
}
Store tokens with EncryptedSharedPreferences (never plain SharedPreferences
or source):
import androidx.security.crypto.EncryptedSharedPreferences
import androidx.security.crypto.MasterKey
class TokenStore(context: Context) {
private val prefs = EncryptedSharedPreferences.create(
context,
"trax_tokens",
MasterKey.Builder(context).setKeyScheme(MasterKey.KeyScheme.AES256_GCM).build(),
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM,
)
fun save(accessToken: String, refreshToken: String?) = prefs.edit()
.putString("access", accessToken)
.putString("refresh", refreshToken)
.apply()
fun access(): String? = prefs.getString("access", null)
fun refresh(): String? = prefs.getString("refresh", null)
fun clear() = prefs.edit().clear().apply()
}
Log out — clear stored tokens and, to end the IdP session, launch the
end-session endpoint (AppAuth's EndSessionRequest, or open
${TraxAuth.END_SESSION}?client_id=…&post_logout_redirect_uri=… in a Custom Tab).
The access token AppAuth hands you is a JWT — attach it as
Authorization: Bearer <access_token> on your /v1 calls (see the OkHttp
interceptor in step 4). This is the credential /v1 accepts as
the user; the media plane's SRT/WHIP/WHEP surfaces use a separate session token
— see the two credential systems. Read the
note on token type and audience
before you swap clients: /v1 validates the JWT against Zitadel and does not yet
check the audience claim.
Prefer to hand-roll it? You can drive a
androidx.browserCustom Tab to the authorize URL yourself, generate the PKCE pair, and POST the token exchange with OkHttp — the same HTTP calls the Swift guide shows. AppAuth is recommended because it gets the redirect handling, PKCE, and refresh right.
4. First call
Add the bearer token with an OkHttp interceptor, then call the generated API:
import live.traxstreaming.api.apis.StudiosApi
import okhttp3.OkHttpClient
import retrofit2.Retrofit
import retrofit2.converter.moshi.MoshiConverterFactory
val http = OkHttpClient.Builder()
.addInterceptor { chain ->
val req = chain.request().newBuilder()
.header("Authorization", "Bearer $apiKey") // or the user's OAuth token
.build()
chain.proceed(req)
}
.build()
val retrofit = Retrofit.Builder()
.baseUrl("https://api-dev.traxstreaming.live/")
.client(http)
.addConverterFactory(MoshiConverterFactory.create())
.build()
val studios = retrofit.create(StudiosApi::class.java)
// List your studios (requires the studios:read scope)
val page = studios.listStudios(limit = 50) // suspend fun (coroutines)
page.data.forEach { println("${it.id} ${it.name}") }
page.nextCursor?.let { cursor ->
// pass `cursor` to fetch the next page — see /reference/pagination
}
Generated method and model names (listStudios, StudiosApi, nextCursor)
come from the spec's operationIds and schemas, matching the
API reference. On a non-2xx response, decode the error body
— it's the one envelope { error: { code, message, requestId } }.
Next
- Errors — branch on
error.code; retry only transient codes. - Pagination · Rate limits.
- Build a client — OAuth (PKCE) login, WebSocket control, and WebRTC (WHIP/WHEP) media for a full producing app on Android.