Android

Beta Versions

Certain WorkOS features may be available only in the beta version of the SDK. Beta versions have the -beta.* suffix, for example, 3.2.0-beta.1. For more information on how to use beta versions, refer to the README in the GitHub repository.

Official Kotlin SDK for the WorkOS API, targeting Android.

Status: pre-release. The spec-driven surface is complete and tested, and the hand-maintained helper layer is implemented apart from the two SSO PKCE helpers that the OpenAPI spec cannot yet express — see Helper coverage.

dependencies {
    implementation("com.workos:workos-android:0.1.0")
}
import com.workos.android.WorkOSClient

val client = WorkOSClient(apiKey = "sk_test_...")

val org = client.organizations.create(name = "Acme")
val page = client.organizations.list(limit = 10)

// Auto-pagination walks every page.
client.organizations.listAutoPaging().collect { println(it.name) }

Every resource method is a suspend fun and takes an optional RequestOptions:

client.organizations.get(
    id = "org_123",
    requestOptions = RequestOptions(
        headers = mapOf("X-Trace-Id" to traceId),
        timeoutSeconds = 10,
        maxRetries = 0,
        idempotencyKey = key,
    ),
)

Errors are typed — the runtime never leaks OkHttp exceptions:

try {
    client.organizations.get(id = "org_missing")
} catch (e: NotFoundException) {
    println("${e.statusCode} ${e.code} ${e.requestId}")
}

Most of this repository is generated by oagen from the WorkOS OpenAPI spec. Do not edit generated files — a CI check blocks PRs that do. Fix the emitter in workos/oagen-emitters (src/android/) and regenerate.

Generated Hand-maintained (@oagen-ignore-file)
models/, enums/, resources/ WorkOSClient.kt, Configuration.kt, RequestOptions.kt
WorkOSClientResources.kt Page.kt, WorkOSException.kt
*Test.kt per resource internal/ (Transport, Json, JsonBody, PathEncoding, AutoPaging)
ModelRoundTripTest.kt helpers/, src/test/.../support/, TransportBehaviorTest.kt

This module currently builds with the Kotlin JVM plugin, not com.android.library. Nothing in the generated SDK touches an Android API — it is OkHttp + kotlinx.serialization + kotlinx.datetime — so the artifact is consumable from Android today, and building it needs no Android SDK. Publishing an AAR is a build-file change, not a source change.

Six capabilities that were once hand-maintained are now in the OpenAPI spec and therefore generated — verified against the live resolved operation table, not assumed:

ID Capability Generated as
H09 authkit_authorization_url userManagement.getAuthorizationUrl
H11 authkit_pkce_code_exchange userManagement.authenticateWithCode(codeVerifier)
H12 authkit_device_flow createDevice + authenticateWithDeviceCode
H13 jwks_helper userManagement.getJwks
H14 sso_authorization_url sso.getAuthorizationUrl
H17 sso_logout_helper sso.getLogoutUrl + sso.authorizeLogout

Hand-maintained in helpers/:

ID Capability Status
Passwordless passwordless.createSession / sendSession
H08 pkce_utilities pkce.generate() / generateCodeVerifier / generateCodeChallenge
H10 authkit_pkce_authorization_url userManagement.getAuthorizationUrlWithPkce
H19 public_client_factory PublicClient.create(clientId)
H01 webhook_verify WebhookVerification().constructEvent(...)
H02 webhook_signature_primitives verifyHeader / createSignature
H03 actions_helper client.actions — verify + sign
H04 session_cookie_object client.session.loadSealedSession(...)authenticate / refresh / getLogoutUrl
H05 session_cookie_inline client.session.authenticateWithSessionCookie / refreshSession
H06 session_cookie_raw_seal Iron.seal / Iron.unseal (Fe26.2, workos-node interop verified)
H07 auth_response_session_sealing client.session.sealAuthResponse(response, password)
H15 sso_pkce_authorization_url sso.getAuthorizationUrlWithPkce(...)
H16 sso_pkce_code_exchange ⛔ blocked — POST /sso/token requires client_secret, which an app cannot hold (see SSO PKCE)
H18 vault_local_crypto client.vaultCrypto.encrypt / .decrypt

H01-H07 and H18 are wire-compatibility-critical: their sealing and signing schemes have to interoperate with the Node/Python/Kotlin SDKs, so each is verified against a cross-SDK fixture rather than written from spec text alone. IronTest opens a seal produced by workos-node, and SessionTest decodes both the workos-node and workos-kotlin cookie payload shapes.

Session cookies

client.session needs a clientId on the client, because verifying a session’s access token means fetching the environment’s JWKS from {baseUrl}/sso/jwks/{clientId}.

val client = WorkOSClient(apiKey = "sk_test_...", clientId = "client_123")

when (val result = client.session.authenticateWithSessionCookie(cookie, cookiePassword)) {
    is AuthenticateSessionResult.Success -> result.user      // verified
    is AuthenticateSessionResult.Failure -> result.reason    // why not
}

authenticate() and refresh() are suspend functions — unlike workos-kotlin, where they block. JWKS retrieval is network I/O, and on Android that must not run on the main thread.

Cookies are written in the camelCase shape workos-node produces, at every level including inside user. They are read permissively, accepting either camelCase or snake_case keys, so cookies sealed by workos-kotlin (which emits snake_case inside user) also open here.

SSO PKCE

sso.getAuthorizationUrlWithPkce adds code_challenge and code_challenge_method to the SSO authorization URL. Both are accepted and S256-enforced by the API but are @ApiHideProperty(), so they never reach the OpenAPI document and cannot be generated — hence a hand-maintained helper, as in every other WorkOS SDK.

The exchange leg does not exist, deliberately. POST /sso/token requires client_secret, validated before any other check:

POST /sso/token  {grant_type, client_id, code}   ->  422
  {"errors":[{"field":"client_secret","code":"client_secret must be a string"}]}

An Android app cannot hold a client secret, so there is no secret-less SSO code exchange to wrap. Until the API accepts code_verifier in place of client_secret, use AuthKit for a complete PKCE flow:

val start = client.userManagement.getAuthorizationUrlWithPkce(redirectUri = "app://callback")
// persist start.codeVerifier across process death, open start.url, then:
val auth = client.userManagement.authenticateWithCode(code = code, codeVerifier = start.codeVerifier)

Public-client usage (Android)

An Android binary cannot hold a WorkOS API key — anything in the APK is extractable. Use PublicClient, which carries an empty key so the full service surface fails loudly rather than appearing to work in development:

val public = PublicClient.create(clientId = "client_123")
val start = public.getAuthorizationUrlWithPkce(redirectUri = "app://callback")
// persist start.codeVerifier across process death, then open start.url
val auth = public.authenticateWithCode(code = code, codeVerifier = start.codeVerifier)
./script/ci     # ktlint + tests

MIT