Shipeasy
SDKsReferenceKotlin

Agent skill

Installable LLM skill for the Kotlin SDK (configure() + Client(user), evaluate, experiment + track, testing).

An installable agent skill for the Kotlin SDK. Fetch it with shipeasy docs skill --sdk kotlin (--install writes it to your agent skills dir), or copy it below — the YAML frontmatter installs with it.

---
name: shipeasy-kotlin
description: Use Shipeasy (feature flags, configs, kill switches, A/B experiments, i18n) from Kotlin/JVM. Covers configure() + Client(user) on servers, ShipeasyClient + configureAndroid for shipped Android apps, getFlag/getConfig/universe(name).assign()/getKillswitch, track, see() error reporting, testing.
---

# Shipeasy Kotlin SDK

Two front doors on the JVM. **`configure()` + `Client(user)`** is the server SDK
(server key, evaluates rules locally — never embed a server key in a shipped
app). **`configureAndroid()`/`configureClient()` + `ShipeasyClient`** is the
native client for shipped Android apps (public client key, server-side eval over
`/sdk/evaluate`, persisted device anon id).

> The documented surface is exactly **`configure()`** (setup) and the bound
> **`Client(user)`** (use), plus the package-level helpers below. For deeper docs,
> fetch any page/snippet from the manifest at
> <https://shipeasy-ai.github.io/sdk-kotlin/manifest.json> (raw URLs below).

## Native Android app? ShipeasyClient (client key)

```kotlin
// build.gradle.kts — Android companion artifact (thin adapter over the core jar):
implementation("ai.shipeasy:shipeasy-kotlin-android:0.21.1")
```

```kotlin
import ai.shipeasy.android.configureAndroid
import ai.shipeasy.shipeasyClient

// Once, in Application.onCreate — PUBLIC client key (pk_…), safe to embed.
// Persists the device anonymous_id across launches (stable bucketing on cold start).
configureAndroid(this, clientKey = BuildConfig.SHIPEASY_CLIENT_KEY)

// From a coroutine (identify is a suspend fun: /sdk/evaluate round-trip + cache):
shipeasyClient()?.identify(mapOf("user_id" to userId, "plan" to "pro"))
val on = shipeasyClient()?.getFlag("new_checkout") ?: false     // cheap cache read
val cta = shipeasyClient()?.universe("hero_cta")?.assign()      // Assignment (auto-logs exposure)
shipeasyClient()?.track("purchase", mapOf("amount" to 49))
shipeasyClient()?.reset()   // logout: keep device anon id, drop user_id
```

Non-Android JVM client, or custom persistence (EncryptedSharedPreferences /
DataStore): `configureClient(clientKey, store = <your AnonStore>)`. Reference:
<https://shipeasy-ai.github.io/sdk-kotlin/pages/installation.md>

The rest of this skill covers the **server** SDK (`configure()` + `Client`).

## Install

```kotlin
implementation("ai.shipeasy:shipeasy-kotlin:0.21.1")
```

## Configure once, evaluate per user

```kotlin
import ai.shipeasy.configure
import ai.shipeasy.Client

// Once at boot. `attributes` maps your user object → the targeting bag.
configure(
    apiKey = System.getenv("SHIPEASY_SERVER_KEY"),               // SERVER key
    attributes = { u -> mapOf("user_id" to (u as MyUser).id, "plan" to u.plan) },
    // poll = true,  // long-running server: keep flags fresh with a background poll
)

// Per request — cheap, user bound at construction (methods take no user arg).
val flags = Client(currentUser)                     // construct once per callsite
flags.getFlag("new_checkout")                       // → Boolean (default=false unless 2nd arg)
flags.getConfig("billing_copy", default = "Pay")    // → Any?
flags.getKillswitch("payments")                     // → true means killed
// Named switch: getKillswitch(name, switchKey) — an unconfigured key falls back
// to the kill switch's top-level value.
```

`configure()` is first-config-wins and owns the fetch lifecycle (one-shot by
default; `poll = true` for a background refresh — you never call `init()`).
Reference: <https://shipeasy-ai.github.io/sdk-kotlin/pages/configuration.md> ·
<https://shipeasy-ai.github.io/sdk-kotlin/pages/flags.md>

## Experiments + track (Client-only, end to end)

Experiments are read by UNIVERSE (a mutual-exclusion pool — the unit lands in ≤1
experiment). `assign()` picks it and resolves params; it's side-effect free. The
exposure fires on the first `get()` read (deduped per process and durably per
`(unit, experiment, group)`); use `peek()` to read a param without logging one.

```kotlin
val flags = Client(currentUser)                     // construct once per callsite
val cta = flags.universe("hero_cta").assign()       // Assignment (no exposure yet)
// cta.name: String? (experiment), cta.group: String?, cta.enrolled: Boolean
// cta.get(field, fallback):  variant override ?? universe default ?? fallback (logs exposure on first read)
// cta.peek(field, fallback): same value, read-only (never logs an exposure)

render(cta.get("primary_label", "Sign up"))         // always safe — first read logs one exposure when enrolled
flags.track("purchase", mapOf("amount" to 49))      // conversion for the bound user
```

Reference: <https://shipeasy-ai.github.io/sdk-kotlin/pages/experiments.md> · track
snippet <https://shipeasy-ai.github.io/sdk-kotlin/snippets/metrics/track.md>

## Error reporting — see()

```kotlin
import ai.shipeasy.see
try { chargeCard(order) }
catch (e: Exception) {
    see(e).causesThe("checkout").to("use backup processor", mapOf("order_id" to order.id))
}
```

`to(outcome, map)` is the terminal — without it nothing is sent; extras ride the
terminal inline. Never `causesThe(x).extras(map).to(y)`: it splits the
consequence sentence in half. `to` returns `Unit`, so extras cannot trail it.
`seeViolation(name)` for non-exceptions;
`controlFlowException(e).because("...")` to mark expected (reports nothing).
Reference:
<https://shipeasy-ai.github.io/sdk-kotlin/pages/error-reporting.md> · snippet
<https://shipeasy-ai.github.io/sdk-kotlin/snippets/ops/see.md>

## Testing (no network)

```kotlin
import ai.shipeasy.configureForTesting
import ai.shipeasy.configureForOffline
import ai.shipeasy.overrideFlag
import ai.shipeasy.clearOverrides
import ai.shipeasy.Client

// Seed values up front; reads go through the ordinary Client(user). Replaces
// prior config, so each test can reconfigure freely.
configureForTesting(
    flags = mapOf("new_checkout" to true),
    configs = mapOf("billing_copy" to "50% off"),
    experiments = mapOf("checkout_button" to ("treatment" to mapOf("color" to "green"))),
)
val flags = Client(mapOf("user_id" to "u_1"))
flags.getFlag("new_checkout")                       // true

overrideFlag("new_checkout", false)                 // flip on the spot
clearOverrides()                                    // drop every override (incl. the seed)

// Offline: evaluate the REAL rules from a snapshot or JSON file, no network.
configureForOffline(path = "shipeasy-snapshot.json")
```

Reference: <https://shipeasy-ai.github.io/sdk-kotlin/pages/testing.md>

## Notes

- **i18n:** the server SDK has no `t()`. Emit the loader tag via package-level
  `i18nScriptTag()` (public client key) for SSR — every argument is optional and
  falls back to `configure(clientKey = ..., profile = ...)`; rendering happens in
  the browser client SDK. Reference:
  <https://shipeasy-ai.github.io/sdk-kotlin/pages/i18n.md>
- **OpenFeature:** no provider bundled — use `getFlag` / `getFlagDetail` directly.
  Reference: <https://shipeasy-ai.github.io/sdk-kotlin/pages/openfeature.md>
- **Advanced:** `privateAttributes`, `stickyStore = InMemoryStickyStore()`,
  `AnonIdFilter` (servlet) for logged-out bucketing, package-level `onChange`
  (requires `poll = true`), `bootstrapScriptTag` SSR (carries the identified
  user as `data-user` so the browser adopts identity on first paint — no
  anon→identified flip), `devtoolsScriptTag()` (hosted overlay: Shift+Alt+S or
  `?se=1`; needs `projectId` + `clientKey` on `configure`). Reference:
  <https://shipeasy-ai.github.io/sdk-kotlin/pages/advanced.md>
- **Quiet outside production (0.16.0+):** the SDK is offline by default when the
  environment isn't production — no fetch, `track`, `see()`, or telemetry until
  you opt in. It's production when the `shipeasy.env` system property or a
  `SHIPEASY_ENV`/`APP_ENV`/`ENV` var is `prod`/`production`, else it falls back to
  the `env` option (defaults to `"prod"`). Force it with
  `configure(..., isNetworkEnabled = true)` or `-Dshipeasy.env=production`.
  Reference: <https://shipeasy-ai.github.io/sdk-kotlin/pages/configuration.md>
Was this page helpful?
Updated July 26, 2026