<!-- llms.txt: https://workos.com/llms.txt -->

## Installation

### Add the WorkOS Android SDK to your app with Gradle

:::code-group{title="Add the WorkOS Android SDK to your app with Gradle"}

```kotlin language="android" title="build.gradle.kts" tab="android-1"
dependencies {
    implementation("com.workos:workos-android:0.1.0")
}
```

:::


# WorkOS Android (Kotlin) SDK

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](#helper-coverage).

## Installation

```kotlin
dependencies {
    implementation("com.workos:workos-android:0.1.0")
}
```

## Usage

```kotlin
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`:

```kotlin
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:

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

## Generated vs. hand-maintained

Most of this repository is generated by [oagen](https://github.com/workos/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` |

## Android target

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.

## Helper coverage

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](#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}`.

```kotlin
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**:

```kotlin
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:

```kotlin
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)
```

## Development

```bash
./script/ci     # ktlint + tests
```

## License

MIT