Mobile & SDKs
Pact SDK for Android / Kotlin
Pure-Kotlin client for Pact's consent-native CRM. Coroutines, kotlinx-serialization, white-label theming, offline writes.
place.pact:pact-sdk is the official Pact client for Kotlin and Android. It
targets JVM 11 and uses Kotlin coroutines + kotlinx-serialization for I/O —
no OkHttp / Retrofit / Ktor dependency to lock you into an HTTP stack.
Install
In your app or library build.gradle.kts:
kotlin
dependencies {
implementation("place.pact:pact-sdk:0.1.0")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.1")
}
Quickstart
kotlin
import kotlinx.coroutines.runBlocking
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.JsonPrimitive
import place.pact.sdk.PactClient
fun main() = runBlocking {
val pact = PactClient(token = System.getenv("PACT_TOKEN"))
pact.onCost = { cost ->
println("cost: ${cost.actualCents}¢ (predicted=${cost.predictedCents})")
}
val list = pact.accounts.list(limit = 5)
println(list.data.data) // typed `List<Account>`
println("consent_filtered: ${list.consentFiltered}")
val briefing = pact.agents.fire(
agentId = "daily_briefing",
input = JsonObject(mapOf("prompt" to JsonPrimitive("weekly review")))
)
println("status=${briefing.data.status} byok=${briefing.data.byok}")
}
The full sample lives in
packages/sdk-android/example-quickstart —
run with PACT_TOKEN=pact_test_… ./gradlew :example-quickstart:run.
In an Android Activity
kotlin
class MainActivity : AppCompatActivity() {
private val pact by lazy {
PactClient(token = BuildConfig.PACT_TOKEN, tenantId = BuildConfig.PACT_TENANT)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
lifecycleScope.launch {
val contacts = pact.contacts.list(limit = 25)
renderContacts(contacts.data.data)
if (contacts.consentFiltered == true) showConsentBadge()
}
}
}
Auth + tenancy
The same pact_live_* / pact_test_* keys (or OAuth access tokens) used by
the JS and Swift SDKs work here.
kotlin
val pact = PactClient(token = "pact_live_…", tenantId = "tenant_uuid")
pact.token = newToken // rotate at runtime
Consent + BYOK + cost
kotlin
val result = pact.agents.fire(agentId = "daily_briefing", input = JsonObject(emptyMap()))
when (result.data.status) {
"consent_blocked" ->
showConsentExplainer(result.data.consentBlockedSubjects.orEmpty())
"ok" -> {
val cost = result.cost
if (cost?.charged == false) {
// BYOK — tenant supplied the LLM credential, Pact charged 0.
}
}
}
Offline writes
kotlin
// Plug in a SharedPreferences-backed Storage to persist across launches:
class PrefsStorage(private val prefs: SharedPreferences) : OfflineQueue.Storage {
override suspend fun load() = prefs.getString("pact.q", null)
?.let { Json.decodeFromString<List<QueuedWrite>>(it) } ?: emptyList()
override suspend fun save(items: List<QueuedWrite>) {
prefs.edit().putString("pact.q", Json.encodeToString(items)).apply()
}
}
pact.setOnline(false) // network observer drives this
try {
pact.activities.log(
subjectType = "contact", subjectId = id,
kind = "call", occurredAt = Instant.now().toString()
)
} catch (e: PactException.QueuedOffline) {
// Mutation queued — surface "Saved offline" toast.
}
pact.setOnline(true) // drains queue automatically
White-label theming
kotlin
val theme = resolveTokens(mapOf("colors" to mapOf("accentEmber" to tenant.brandColor)))
val emberInt = pactHexToColorInt(theme.colors.accentEmber)
button.setBackgroundColor(emberInt)
Compatibility notes
| Concern | Behaviour |
|---|---|
| Kotlin / JVM | Built against Kotlin 1.9.24, JVM 11. |
| Coroutines | Public methods are suspend; the request pipeline runs on Dispatchers.IO. |
| Serialization | kotlinx.serialization.json for the wire format. |
| ProGuard / R8 | No reflection used; the entity decoders are explicit pure functions. |
| Compose Multiplatform | Library is pure-JVM with no Android dependencies; consume directly from commonMain. |