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