The WorkOS Kotlin library provides convenient access to the WorkOS API from applications written in JVM-compatible languages (Kotlin, Java, Scala, etc.).
See the API Reference for Kotlin/Java usage examples. See the v5 migration guide when upgrading from the pre-generated SDK surface.
Replace VERSION in the snippets below with the latest release from Maven Central.
<dependency>
<groupId>com.workos</groupId>
<artifactId>workos</artifactId>
<version>VERSION</version>
</dependency>dependencies {
implementation 'com.workos:workos:VERSION'
}dependencies {
implementation("com.workos:workos:VERSION")
}- JDK 17+
- Kotlin 2.1.x or newer
Construct a WorkOS client by passing your API key. All other parameters are optional.
import com.workos.WorkOS
import com.workos.common.http.RetryConfig
val workos = WorkOS(
apiKey = System.getenv("WORKOS_API_KEY"),
// Required for several user-management auth flows (token exchange, etc.)
clientId = System.getenv("WORKOS_CLIENT_ID"),
// Defaults to "https://api.workos.com". Override for staging or proxies.
apiBaseUrl = "https://api.workos.com",
// Optional — defaults to a 30s-timeout OkHttpClient.
// httpClient = myCustomOkHttpClient,
// Retry/backoff policy. Defaults to exponential backoff with 3 retries.
retryConfig = RetryConfig.DEFAULT,
)| Parameter | Required | Description |
|---|---|---|
apiKey |
yes | WorkOS secret key (sk_*). |
clientId |
no | Required for user-management auth flows and public-client usage. |
apiBaseUrl |
no | Defaults to https://api.workos.com. Override for staging/local. |
httpClient |
no | Bring-your-own OkHttpClient for telemetry, interceptors, etc. |
retryConfig |
no | See RetryConfig.DEFAULT / RetryConfig.DISABLED. |
Service accessors are regular properties on the WorkOS class, so they work natively from Java:
import com.workos.WorkOS;
WorkOS workos = new WorkOS(System.getenv("WORKOS_API_KEY"));
workos.getUserManagement().list();For more configurable construction, WorkOS.builder() exposes a fluent
builder so Java callers can set only the parameters they care about:
import com.workos.WorkOS;
import com.workos.common.http.RetryConfig;
WorkOS workos = WorkOS.builder()
.apiKey(System.getenv("WORKOS_API_KEY"))
.clientId(System.getenv("WORKOS_CLIENT_ID"))
.retryConfig(RetryConfig.DISABLED)
.build();Generated model and exception properties are exposed as Kotlin vals with
@JvmField, which means Java callers read them as fields rather than
through bean-style getters — connection.id, not connection.getId().
Sealed-class operation parameters (e.g. ResourceTarget) include
Java-friendly overloads that take the discriminating fields directly
(workos.getAuthorization().checkByExternalId(...)) so you do not have to
construct the variant class explicitly.
Once constructed, every WorkOS API surface is available as a property on the WorkOS instance.
workos.ssoworkos.userManagementworkos.userManagementOrganizationMembershipGroupsworkos.passwordless†workos.multiFactorAuth
workos.organizationsworkos.organizationDomainsworkos.directorySyncworkos.groups
workos.authorizationworkos.featureFlags
workos.auditLogsworkos.eventsworkos.webhooksworkos.radar
workos.connectworkos.pipesworkos.widgetsworkos.apiKeysworkos.adminPortalworkos.actions†workos.vault†
† Provided as a Kotlin extension property. From Java, access these via the
generated static accessor on the corresponding *Kt file
(e.g. PasswordlessKt.getPasswordless(workos)) instead of as a member of
WorkOS.
import com.workos.WorkOS
val workos = WorkOS(System.getenv("WORKOS_API_KEY"))
val org = workos.organizations.create(
name = "Foo Corp",
)
println("Created ${org.id}")
// Auto-paginate through every event, one page at a time.
val events = workos.events.list(events = listOf("dsync.user.created"))
for (event in events.autoPagingIterable()) {
println(event.id)
}Every API error deserializes into a typed subclass of
com.workos.common.exceptions.WorkOSException:
| HTTP status | Exception |
|---|---|
| 400 | com.workos.common.exceptions.BadRequestException |
| 401 | com.workos.common.exceptions.UnauthorizedException |
| 404 | com.workos.common.exceptions.NotFoundException |
| 422 | com.workos.common.exceptions.UnprocessableEntityException |
| 429 | com.workos.common.exceptions.RateLimitException |
| 5xx | com.workos.common.exceptions.GenericServerException |
| other | com.workos.common.exceptions.WorkOSGenericException |
| transport | com.workos.common.exceptions.TransportException |
TransportException (status 0) is thrown when the SDK cannot reach the
API at all — IOExceptions, timeouts, DNS or TLS failures.
WorkOSGenericException is thrown when the API returns an HTTP status the
SDK does not have a more specific exception type for.
Each exception exposes the response code, human-readable message, and
the structured errors: List<ApiError>? array returned by the API.
ApiError is a typed model (field, code, message) so callers can
inspect validation failures without Map casts.
RateLimitException additionally exposes retryAfterSeconds parsed from
the Retry-After response header (delta-seconds or HTTP-date) when the
server provides one. WorkOSException.rawBody also preserves the raw HTTP
response body for debugging; treat it as sensitive because it can contain
PII or secrets that should be redacted before logging.
import com.workos.common.exceptions.NotFoundException
try {
workos.organizations.get("org_nope")
} catch (e: NotFoundException) {
println("Not found: ${e.message}")
}The standalone Webhook helper validates signatures on inbound webhook
payloads. Two flavors are available:
constructTypedEvent(...)returns a sealedWorkOSEventyou can pattern-match against — preferred for the common case.constructEvent(...)returns a rawJsonNode, which is useful if you need to handle event types the SDK does not yet model.
import com.workos.models.WorkOSEvent
import com.workos.webhooks.Webhook
val event = Webhook().constructTypedEvent(
payload = rawRequestBody,
signatureHeader = request.getHeader("WorkOS-Signature"),
secret = System.getenv("WORKOS_WEBHOOK_SECRET"),
)
when (event) {
is WorkOSEvent.UserCreated -> println("New user ${event.data.id}")
else -> println("Other event ${event.event}")
}PATCH endpoints distinguish between "field omitted", "field set to a
value", and "field explicitly cleared to null". Use PatchField to
express that intent — see the
v5 migration guide
for examples.
Every generated service method has both a blocking version and a
coroutine-aware <method>Suspend variant that wraps the call in
withContext(Dispatchers.IO). Add kotlinx-coroutines-core to your
classpath (already a transitive dependency of this SDK) and call from any
suspend context:
val org = workos.organizations.createSuspend(name = "Foo Corp")
val firstPage = workos.organizations.listSuspend(limit = 10)
for (org in firstPage.data) println(org.id)The blocking and suspend forms are independently named (create vs
createSuspend) so they can coexist on the same service without Kotlin
overload-resolution ambiguity.
All list(...) methods return a Page<T>. You can either consume a single
page directly, or walk every page via the auto-paging iterator.
val firstPage = workos.organizations.list(limit = 10)
for (org in firstPage.data) println(org.id)
// Walk the full result set.
for (org in firstPage.autoPagingIterable()) println(org.id)Every generated method accepts an optional RequestOptions argument for
per-call overrides:
import com.workos.common.http.RequestOptions
workos.organizations.create(
name = "Foo Corp",
requestOptions =
RequestOptions.builder()
.idempotencyKey(java.util.UUID.randomUUID().toString())
.maxRetries(0)
.build(),
)Generated enums include an Unknown variant annotated with Jackson's
@JsonEnumDefaultValue. When the API introduces a new wire value the SDK
has not yet been updated for, deserialization falls back to Unknown
instead of throwing. Callers that need to preserve the raw value should
inspect the underlying JSON directly until the SDK is regenerated.
This SDK follows SemVer. Breaking changes only ship in major releases — read the changelog before upgrading across a major version boundary.