Graine AI

Kotlin Multiplatform SDK reference

Every public class, function, property, event and error of ai.graine:inapp-kmp 0.8.0 and ai.graine:inapp-kmp-compose 0.8.0, grouped by what you use it for: signatures, parameters, what each returns, the errors it raises, and what to expect.

Every public declaration of the Kotlin Multiplatform SDK — ai.graine:inapp-kmp 0.8.0, one API in shared code over Graine's Android and iOS SDKs, and ai.graine:inapp-kmp-compose 0.8.0, the SDK's own screens for Compose Multiplatform — grouped by what you use it for. Each entry says what it is for, then gives its signature, its parameters, what it returns, the errors it can raise and an example; each type lists its fields with what to expect. The same API across every platform, topic by topic, is the SDK reference; installing and setting up is Kotlin Multiplatform.

Requirements, install, permissions and initialisation

Artifactsai.graine:inapp-kmp:0.10.3 (the shared API) and ai.graine:inapp-kmp-compose:0.10.3 (the chat, the call screen and the voice bar for Compose Multiplatform), on Maven Central
RequiresKotlin 2.1.20 or newer; targets androidTarget, iosArm64, iosSimulatorArm64 and iosX64; Android 7.0 (API 24) and up; iOS 15 and up. Depends on kotlinx-coroutines and kotlinx-datetime only (and Compose, for the compose artifact)
How it worksOne API in shared code over the native SDKs: the Android SDK (ai.graine:inapp-android 0.8.0) comes with it; on iOS your app links the iOS SDK (GraineInApp 0.8.0) itself, so it is never in the app twice
In the dashboardAdd app:// to the agent's Allowed domains (Agent → Embed & Widgets); without it every request is refused

Gradle (Kotlin DSL) — shared/build.gradle.kts

kotlin {
    androidTarget()
    iosArm64(); iosSimulatorArm64(); iosX64()
    sourceSets {
        commonMain.dependencies {
            implementation("ai.graine:inapp-kmp:0.10.3")
            implementation("ai.graine:inapp-kmp-compose:0.10.3")
        }
    }
}
  • Android. The Android SDK's manifest comes with it — the microphone and notification permissions, the call screen, the call service — so there is nothing to copy (what it adds, and how to remove the calls entries from an app without calls, is on the Android reference). The library finds your application by itself, so Graine.configure(config) works from shared code. Ask for POST_NOTIFICATIONS on Android 13 and up during onboarding — without it the phone cannot ring. An activity that shows the voice bar or places calls forwards its permission answers to Graine.onRequestPermissionsResult(requestCode, grantResults). For calls Graine rings, add Firebase Cloud Messaging to the Android app (google-services.json, its Gradle plugin and com.google.firebase:firebase-messaging): the SDK registers for pushes itself. An app with a FirebaseMessagingService of its own, which outranks the SDK's, calls Graine.calls.onNewToken(token) and Graine.calls.handlePush(message.data) from it.
  • iOS. Add the iOS SDK to the app: with Swift Package Manager (https://github.com/GraineAI/inapp-ios, from 0.10.3) and your shared framework built static (isStatic = true, the Compose Multiplatform default), or with CocoaPods (pod 'GraineInApp'; with Kotlin's CocoaPods plugin and a dynamic framework, pod("GraineInApp") { version = "0.10.3"; linkOnly = true }). Add NSMicrophoneUsageDescription to Info.plist, and for calls Push Notifications and Background Modes → Audio and Voice over IP. Call Graine.calls.enableVoIPPushes() at every launch. Link GraineInApp 0.10.3, the version of this library: with an older one (0.7.0), booking is not available on iOS — bookingAvailable is false and presentBooking throws Unavailable — and everything else works.

Kotlin Multiplatform · Kotlin — commonMain

Graine.configure(GraineConfig(
    baseUrl = "https://www.graine.ai",
    publishableKey = "pk_live_…",          // Agent → Embed & Widgets → Install
    appVersion = "2.4.0",
))
Graine.calls.configure(GraineCallsConfig(displayName = "Acme"))
 
// Compose Multiplatform UI
Box(Modifier.fillMaxSize()) {
    AppContent()
    GraineAgentBar(Modifier.align(Alignment.BottomCenter).fillMaxWidth().height(180.dp))
}

Every function may be called from any thread; the suspending ones never block the main thread; the states are flows that always hold the latest value.

WebRTC calls (optional)

Native calls carry their audio over a WebSocket — in both directions: the person calling the agent (startCall, your “Call us” button) and the person answering a call Graine rang (a booked call, Ring now). Add each platform's WebRTC module to the app and they carry it over WebRTC instead. Nothing in shared code changes: the same ring, the same call screen, the same “Connecting…”, the same outcomes.

Gradle (Kotlin DSL) — shared/build.gradle.kts, the Android side

kotlin {
    sourceSets {
        androidMain.dependencies {
            implementation("ai.graine:inapp-android-rtc:0.10.3")   // the same version as ai.graine:inapp-android
        }
    }
}
  • Android. ai.graine:inapp-android-rtc, at the same version as the Android SDK this library brings (ai.graine:inapp-android 0.8.0) — in androidMain as above, or in the Android app's own dependencies. Its manifest adds ACCESS_NETWORK_STATE, a normal permission granted at install: nothing is asked. Its R8 rules ship inside it. Size per phone (an app bundle delivers one ABI): about 7.4 MB installed on 64-bit ARM (about 2.5–3.3 MB to download), 4.3 MB on 32-bit ARM, 8.2–8.4 MB on x86, and about 0.5 MB of classes.
  • iOS. The GraineInAppRTC package, added to the iOS app target beside GraineInApp, the same way and at the same version: with Swift Package Manager https://github.com/GraineAI/inapp-ios-rtc (its library GraineInAppRTC), or with CocoaPods pod 'GraineInAppRTC', :git => 'https://github.com/GraineAI/inapp-ios-rtc.git', :tag => '0.10.3'. No code, no Info.plist entry, no capability: the calls find the module by themselves. A build that strips code nothing references can call GraineRTC.install() at launch from the app's Swift code (import GraineInAppRTC); GraineRTCVersion.name is the module's version. About 12 MB more installed on an arm64 iPhone, about 6 MB more to download. If another package in the app pins the same WebRTC build at a different version, the two cannot be resolved together: leave the module out (calls stay on the WebSocket) or align the versions.
  • Choosing. GraineCallsConfig(callTransport = …), a GraineCallTransport: AUTO, the default — WebRTC when the module is in the app and the dashboard allows it, else the WebSocket; WEBSOCKET — always the WebSocket; WEBRTC — WebRTC wherever it can carry the call (the WebSocket only when the module is missing or WebRTC cannot set up this call).
  • The dashboard's switch. The agent's In-app call audio setting (agent → Calls & scheduling): WebRTC (recommended) or Standard. Standard moves every install's calls back to the WebSocket, whatever the app chose, from each app's next start — no app release.
  • What the person notices. On a weak or changing network the agent's voice holds up instead of stalling and catching up: a jitter buffer, loss concealment and the platform's echo canceller. The call is prepared at the tap, or while a ring is showing, so it connects as fast as before. Over WebRTC the agent's voice arrives at telephone bandwidth today: steadier through a weak network, narrower than the WebSocket's on a perfect one.
  • When WebRTC cannot set up a call before any audio flows — a network that blocks it, a server that turns it down, no answer within the time allowed (about 12 seconds in all) — the call continues on the WebSocket inside the same call: no second ring, no new call screen, no error; “Connecting…” shows a moment longer. A call the agent cannot take at all (no credit left, the agent switched off) ends with that reason, exactly as without the module. Once audio flows, a call never moves: a network drop is retried once, then the call ends as dropped. Graine.calls.state says which one carried a call — transport and transportFallbackReason (GraineCallInfo).
  • Licences. A prebuilt WebRTC library (Google's WebRTC, BSD 3-Clause, its symbols renamed so it never clashes with another copy in the app): list it in your app's open-source notices. Android: graine-inapp-rtc/THIRD-PARTY-NOTICES.md; iOS: the package's THIRD-PARTY-NOTICES.md.

Kotlin Multiplatform · Kotlin — commonMain

Graine.calls.configure(GraineCallsConfig(displayName = "Acme", callTransport = GraineCallTransport.AUTO))

Install and the first answer

Configure the SDK once at launch, then read the agent's settings when they arrive. Calling anything on Graine before configure throws IllegalStateException (“Graine.configure(GraineConfig(…)) has not been called. Call it once, at launch.”).

Graine.configure

Call it once at launch — in Application.onCreate on Android, the app's init on iOS — to start the agent.

fun configure(config: GraineConfig, name: String = DEFAULT_AGENT): GraineAgent

Without a name it starts the default agent, which everything on Graine belongs to. An app with several agents configures each under its own name and key (Several agents). Asking again for a name already set up gives you that same agent; the configuration passed the second time is ignored. On Android the SDK finds the application by itself; on iOS the agent is the iOS SDK's agent under that name — one the app already configured from Swift is used as it is.

ParameterTypeDefaultMeaning
configGraineConfigrequiredThe agent's publishable key, the base URL and the options (GraineConfig).
nameStringDEFAULT_AGENTThe name to configure the agent under — leave it out for the default agent, which is "default"; blank is the default too.

Returns — GraineAgent — the agent (GraineAgent).

Errors — IllegalStateException on Android when the application cannot be found (“Graine could not find the application. Keep ai.graine.inapp.kmp.GraineInitProvider in the manifest, or call Graine.configure(context, config) on Android.”), and on iOS when the iOS SDK cannot configure the agent.

Graine.configure(GraineConfig(baseUrl = "https://www.graine.ai", publishableKey = "pk_live_…"))

Graine.configure (Android, with a context)

Use it on Android only when your app removed GraineInitProvider from its manifest, so the SDK cannot find the application itself.

fun Graine.configure(context: Context, config: GraineConfig, name: String = Graine.DEFAULT_AGENT): GraineAgent

The same as Graine.configure(config, name), with the application taken from context. In androidMain only.

ParameterTypeDefaultMeaning
contextContextrequiredAny Android context of your app — the Application or an Activity.
configGraineConfigrequiredThe agent's key, base URL and options.
nameStringGraine.DEFAULT_AGENTThe name to configure the agent under; the default agent when left out.

Returns — GraineAgent.

Errors — none.

import android.app.Application
 
class AcmeApp : Application() {
    override fun onCreate() {
        super.onCreate()
        Graine.configure(this, GraineConfig(baseUrl = "https://www.graine.ai", publishableKey = "pk_live_…"))
    }
}

Graine

Use it for everything: it is the SDK, and the default agent.

object Graine

Configure it once at launch. Who the user is, this install, the memory choice, the screen and the phone's calls belong to the app and are shared by every agent; everything else on Graine is the default agent's.

What to expect — Every member may be called from any thread; calling anything but configure before it throws IllegalStateException. The flows always hold the latest value; events is not replayed.

PropertyTypeMeaning
const val DEFAULT_AGENT = "default"StringUse it to name the default agent explicitly — Graine.agent(Graine.DEFAULT_AGENT).
val agents: Map<String, GraineAgent>Map<String, GraineAgent>Read it to list every configured agent, by name, in the order they were configured.
val isConfigured: BooleanBooleanRead it to know whether the default agent has been configured — before calling anything else from code that may run first.
val session: StateFlow<GraineSession?>StateFlow<GraineSession?>Collect it to know when the default agent's settings have loaded (Graine.session); null before.
val theme: StateFlow<GraineThemeTokens?>StateFlow<GraineThemeTokens?>Collect it to draw UI of your own in the brand's design: the default agent's design tokens for the phone's light or dark appearance (Graine.theme); null before the settings load.
val events: SharedFlow<GraineEvent>SharedFlow<GraineEvent>Collect it, from app start, to hear what the default agent reports that is not a state (GraineEvent). Not replayed.
val chat: GraineChatGraineChatUse it to drive the default agent's chat (GraineChat).
val voice: GraineVoiceGraineVoiceUse it to drive the default agent's voice-bar call (GraineVoice).
val calls: GraineCallsGraineCallsUse it for native calls — the app's, whichever agent a call is with (GraineCalls).
val schedulingRules: GraineSchedulingRulesGraineSchedulingRulesRead it to know what the default agent lets users book (GraineSchedulingRules); the defaults, with booking off, before the settings load.
val bookingAvailable: BooleanBooleanRead it to decide whether to offer booking for the default agent (Graine.bookingAvailable).
val installId: StringStringRead it for this install's ID: a random UUID made on first launch and kept for as long as the app is installed; the same one the native SDKs use, for every agent.
val memoryEnabled: BooleanBooleanRead it to show the user's memory choice: their last choice on this install, else true.

Its functions: configure, agent, identify, setMemory, memories, forgetMemory, setVariables, currentVariables, registerAction, setScreen, updateField, removeField, clearScreen, track, tap, noteInteraction, presentBooking, scheduleCall, scheduledCalls, cancelScheduledCall, onRequestPermissionsResult — each with its parameters, what it returns, its errors and an example.

Graine.session (the agent's settings)

Collect it to know when the agent's settings have loaded — before offering what depends on them, such as booking.

val session: StateFlow<GraineSession?>

null until the default agent's settings arrive from Graine (once per launch), then the GraineSession. Each agent has its own: GraineAgent.session. The same moment is also an event, GraineEvent.SessionReady.

Parameters — none.

Returns — StateFlow<GraineSession?>. Example value: GraineSession(agentId="a7c1…", memoryEnabled=true, identityField="user_id", devicesPerUser=2, schedulingRules=GraineSchedulingRules(usersCanBook=true, …), declaredActions=[open_screen]).

Errors — none.

var callMeVisible = false   // whether your "Call me" button shows
scope.launch {
    Graine.session.filterNotNull().first()
    callMeVisible = Graine.bookingAvailable
}

GraineConfig

Pass it to Graine.configure to say which agent to start and how.

data class GraineConfig(…)

Everything the SDK needs to know up front — the native SDKs' configuration, field for field.

What to expect — Made once, at launch. Optional fields have the defaults shown; nothing in it is sent to Graine except what the descriptions say.

FieldTypeDefaultMeaning
baseUrlStringrequiredWhere Graine is: "https://www.graine.ai" unless your workspace is self-hosted.
publishableKeyStringrequiredSelects the agent: its publishable key from Agent → Embed & Widgets. Not a secret, and it cannot place a call on its own.
variablesMap<String, Any?>emptyMap()Prompt variables your app knows at build time. Values from identify() and setVariables() take precedence over these.
appVersionString?nullUse it to tell builds apart: it lands on the call record.
endOnBackgroundBooleantrueEnds a live voice-bar conversation when the app goes to the background. Keep it on.
debugBooleanfalseTurn it on in debug builds to log the SDK's traffic with Graine — keys only, never values — to logcat (tag Graine) and the iOS console.
captureTapsBooleantrueLets the SDK notice taps on labelled controls (what a screen reader reads, a view's ID, a button's words) by itself, at most two a second and only while a screen is reported, so the agent knows what the user just pressed. It only watches.
transportGraineTransport?nullUse it to force how the voice bar's audio travels (GraineTransport); null lets the dashboard's Call audio setting decide.
chatSizeGraineChatSize?nullHow big the chat window opens in this app, over the dashboard's Window size (GraineChatSize); a size passed to chat.open(size) wins for that open.

GraineSession

Read it to know what the agent's settings say once they have loaded — who the user is, the booking rules, the declared actions.

data class GraineSession(…)

From Graine.session (and GraineAgent.session, GraineEvent.SessionReady).

What to expect — Loaded once per launch; null in session until then. A setting an older server does not send means its default: the user_id trait, 2 devices per user, booking off.

FieldTypeDefaultMeaning
agentIdStringrequiredUse it in logs and support requests: the agent's ID in Graine.
memoryEnabledBoolean?requiredRead it to know whether the agent remembers its users; null when Graine does not say.
identityFieldString"user_id"Read it to know which trait names the user (Agent → Memory → Who is the user?): the one identify must carry for memory and booking to work.
devicesPerUserInt2How many devices one user may use the agent on (1–5); beyond it, device_limit.
schedulingRulesGraineSchedulingRulesGraineSchedulingRules()Read it for the booking rules and how Graine rings (GraineSchedulingRules).
declaredActionsSet<String>emptySet()Use it to check your handlers against the actions declared on the agent: one declared and not registered is refused when the agent asks for it.

GraineInitProvider

Leave it in your merged manifest on Android: it is how Graine.configure(config) finds the application from shared code.

class GraineInitProvider : ContentProvider()

Declared by the library and started by Android at process start, before Application.onCreate; it records the application and, from then on, the activity in front. It serves no data. An app that removes it calls Graine.configure(context, config) instead.

What to expect — Nothing to call. Android only.

Identity, memory and conversations that continue

Tell the agent who the user is, so it can remember them, ring them, and continue their conversation. Where the agent remembers its users, a signed-in user who comes back finds the chat where they left it: it opens with the earlier messages (restored, oldest first; an earlier call's as one “Voice call” row) and the agent continues from them. chat.newConversation() starts afresh. Who the user is belongs to the app: Graine.identify tells every agent.

Graine.identify

Call it after the user signs in, and with null when they sign out.

fun identify(traits: Map<String, Any?>?, memory: Boolean? = null)

The trait the agent's Who is the user? setting names (user_id unless the dashboard chose email, phone or a custom trait) is how Graine tells your users apart: it lets the agent remember a user and ring them. Its value goes only to Graine and never becomes a prompt variable; every other trait is masked on the device and reaches the agent as a prompt variable. A placeholder ("guest", a masked value) identifies nobody.

It applies to the next conversation, for every agent. null signs out everywhere: this device stops being one of the user's, the chat on screen is cleared, and later conversations are anonymous. Identifying a different user does the same for the previous one.

ParameterTypeDefaultMeaning
traitsMap<String, Any?>?requiredWho the user is, by trait name — mapOf("user_id" to user.id, "name" to user.firstName). null signs out.
memoryBoolean?nullfalse keeps this user's conversations out of memory — nothing is remembered or recalled — and the choice is saved on this install for that user. null keeps their last choice — on until they make one.

Returns — nothing.

Errors — IllegalStateException before configure.

Graine.identify(mapOf("user_id" to user.id, "name" to user.firstName), memory = user.allowsMemory)
Graine.identify(null)   // on sign-out

Graine.setMemory

Call it when the user changes their memory choice in your privacy settings.

fun setMemory(enabled: Boolean)

The same as identify with the same traits and memory = enabled, for every agent. Kept on this install for this user.

ParameterTypeDefaultMeaning
enabledBooleanrequiredtrue to remember this user's conversations, false to stop.

Returns — nothing.

Errors — IllegalStateException before configure.

Graine.setMemory(false)

Graine.memories

Call it to show the user what the agent remembers about them, on a privacy screen.

suspend fun memories(): List<GraineMemory>

What the default agent remembers about the signed-in user, newest first, at most 100, already redacted. Only from a device the user is using. GraineAgent.memories() asks another agent.

Parameters — none.

Returns — List<GraineMemory> (GraineMemory). Example value: [GraineMemory(id="m_31", text="Prefers a call after 6 PM", createdAt=2026-10-12T09:15:00Z, updatedAt=null)].

Errors — throws GraineException: IdentityRequired with nobody signed in, DeviceLimit from a device that is not one of the user's, RateLimited, Network, Unavailable, InvalidKey, OriginRefused.

val remembered = try { Graine.memories() } catch (e: GraineException) { emptyList() }

Graine.forgetMemory

Call it when the user asks the agent to forget them.

suspend fun forgetMemory(): Int

Deletes everything the default agent remembers about the signed-in user — only from a device that user is using. Once it succeeds the chat on screen is cleared too; a refusal leaves it as it was.

Parameters — none.

Returns — Int — how many memories were deleted. Example value: 3.

Errors — throws GraineException: IdentityRequired, DeviceLimit, RateLimited, Network, Unavailable, InvalidKey, OriginRefused.

val deleted = Graine.forgetMemory()

GraineAgent.memories

Call it to show what one of your other agents remembers about the signed-in user.

suspend fun memories(): List<GraineMemory>

Graine.memories() for this agent: newest first, at most 100, already redacted.

Parameters — none.

Returns — List<GraineMemory>.

Errors — throws GraineException as Graine.memories(); IllegalStateException when this agent is not configured.

val remembered = Graine.agent("support")?.memories().orEmpty()

GraineAgent.forgetMemory

Call it to make one of your other agents forget the signed-in user.

suspend fun forgetMemory(): Int

Graine.forgetMemory() for this agent.

Parameters — none.

Returns — Int — how many memories were deleted.

Errors — throws GraineException as Graine.forgetMemory().

val deleted = Graine.agent("support")?.forgetMemory() ?: 0

GraineChat.newConversation

Call it to give the user a fresh conversation instead of continuing the last one.

suspend fun newConversation()

The chat hangs up, Graine lets go of the conversation it kept for this agent and user (so the next chat does not continue it), and the messages on screen and any waiting to go are cleared; when the user was in the chat, a fresh chat opens with the welcome and the starters. Memories the agent keeps about the user stay as they are. With nobody signed in there is nothing kept to let go of, so only the screen is cleared. The SDK's chat window calls it from the New conversation button in its header, which it shows only while a conversation continued from before is on screen.

Parameters — none.

Returns — nothing, once the new conversation has started.

Errors — throws GraineException when Graine refused (DeviceLimit, Network, …); the chat is then as it was.

scope.launch { runCatching { Graine.chat.newConversation() } }

GraineMemory

Read it to show one thing the agent remembers about the user.

data class GraineMemory(val id: String, val text: String, val createdAt: Instant? = null, val updatedAt: Instant? = null)

What to expect — From memories(): newest first, at most 100, already redacted. The times are moments; show them in the device's time zone.

FieldTypeDefaultMeaning
idStringrequiredUse it as a stable key when you list memories.
textStringrequiredShow it: what is remembered, in a sentence, already redacted.
createdAtInstant?nullWhen it was first remembered; null when Graine did not say.
updatedAtInstant?nullWhen it last changed; null when it never did or Graine did not say.

Chat

The typed conversation with an agent: Graine.chat for the default agent, Graine.agent("support")?.chat for another. Show the SDK's own chat screen — GraineChat() in Compose, GraineViews.chat() in UIKit — or draw your own from turns, status, typing and upcomingCalls. Everything works as on a call: the agent sees the screen you report, runs your actions and draws its components. Only one conversation runs at a time — opening the chat ends a voice-bar call, and during a native call the chat waits until the call is over.

GraineChat

Use it to open, send to and follow one agent's chat.

class GraineChat

Graine.chat, or GraineAgent.chat for another agent. A message typed while the chat is not connected is shown at once and sent when it is; a dropped connection is made again on its own while the user is in the chat.

What to expect — The four flows always hold the latest value. turns empties on sign-out, when another user signs in, after a successful forgetMemory(), on newConversation() and on clear().

PropertyTypeMeaning
val turns: StateFlow<List<GraineChatTurn>>StateFlow<List<GraineChatTurn>>Collect it to draw the conversation, oldest first (GraineChatTurn).
val status: StateFlow<GraineChatStatus>StateFlow<GraineChatStatus>Collect it to show the connection — Online, Connecting… (GraineChatStatus).
val typing: StateFlow<Boolean>StateFlow<Boolean>Collect it to draw a typing indicator: true from the user's message until the agent's first words.
val upcomingCalls: StateFlow<List<GraineScheduledCall>>StateFlow<List<GraineScheduledCall>>Collect it to draw the SDK window's Upcoming calls card in a chat of your own: the user's calls still to come, soonest first, at most three (GraineScheduledCall). Fetched when the chat opens and after the agent finishes a reply (it may have just booked one), only where Graine rings this app; empty otherwise. On iOS it needs GraineInApp of this version; with an older one it stays empty.

Its functions: open, close, send, retry, clear, newConversation, refreshUpcomingCalls, cancelUpcomingCall, answerWidget — each with its parameters, what it returns, its errors and an example.

GraineChat.open

Call it when the user goes into the chat, so it connects and stays connected.

fun open(size: GraineChatSize? = null)

Ends a call the voice bar holds. A voice bar on screen moves to its chat and draws it at size — this open's own, until close(); null follows GraineConfig.chatSize, then the dashboard's Window size.

ParameterTypeDefaultMeaning
sizeGraineChatSize?nullHow big the chat opens this time: COMPACT, LARGE or FULL.

Returns — nothing.

Errors — IllegalStateException when the agent is not configured.

Graine.chat.open(GraineChatSize.LARGE)

GraineChat.close

Call it when the user leaves the chat.

fun close()

The chat hangs up and stays down; what was said stays.

Parameters — none.

Returns — nothing.

Errors — IllegalStateException when the agent is not configured.

Graine.chat.close()

GraineChat.send

Call it to send the user's message from a chat screen of your own.

fun send(text: String)

Shown at once, and sent now or as soon as the chat is connected. Blank messages are ignored.

ParameterTypeDefaultMeaning
textStringrequiredWhat the user typed.

Returns — nothing.

Errors — IllegalStateException when the agent is not configured.

Graine.chat.send("Where is my order?")

GraineChat.retry

Call it from a “Try again” control when the chat is offline or a message failed.

fun retry()

Connects now and sends what could not be sent.

Parameters — none.

Returns — nothing.

Errors — IllegalStateException when the agent is not configured.

Graine.chat.retry()

GraineChat.clear

Call it to empty the chat on screen — a new topic, a tidy screen.

fun clear()

Only the screen: the conversation goes on — connected stays connected — a message still waiting for the connection is still sent, and nothing Graine keeps is touched: the conversation that continues comes back with the next history, and what the agent remembers stays. newConversation() starts over; forgetMemory() forgets. On iOS it needs GraineInApp of this version; an older one ignores it.

Parameters — none.

Returns — nothing; turns is empty.

Errors — IllegalStateException when the agent is not configured.

Graine.chat.clear()

GraineChat.refreshUpcomingCalls

Call it after the user booked a call outside the chat, so the Upcoming calls card of your own chat shows it.

fun refreshUpcomingCalls()

Fetches upcomingCalls again. The chat refreshes them by itself when it opens and after each of the agent's replies, so most apps never call it. On iOS it needs GraineInApp of this version; an older one does nothing.

Parameters — none.

Returns — nothing; upcomingCalls changes when Graine answers.

Errors — IllegalStateException when the agent is not configured.

Graine.chat.refreshUpcomingCalls()

GraineChat.cancelUpcomingCall

Call it from the Cancel control on a row of your own Upcoming calls card.

fun cancelUpcomingCall(id: String)

The call leaves upcomingCalls once Graine has called it off; if Graine refuses, it stays in the list. A blank id is ignored. To learn the outcome yourself, call Graine.cancelScheduledCall(id) instead (Graine.cancelScheduledCall). On iOS it needs GraineInApp of this version; an older one does nothing.

ParameterTypeDefaultMeaning
idStringrequiredThe call's id, from upcomingCalls.

Returns — nothing.

Errors — IllegalStateException when the agent is not configured.

Graine.chat.upcomingCalls.value.firstOrNull { it.canCancel }?.let { Graine.chat.cancelUpcomingCall(it.id) }

GraineChatTurn

Read it to draw one message of the chat.

data class GraineChatTurn(…)

What to expect — One per message, oldest first. The agent's message updates in place (same id) while live, then stops. A user's message typed while the chat is not connected is SENDING and goes as soon as it is; FAILED means it could not reconnect — retry() tries again. Restored turns arrive once, when the chat opens, for a signed-in user on an agent with memory on: at most 50, oldest first, delivered, never live, with their original times; the welcome is not sent then.

FieldTypeDefaultMeaning
idStringrequiredUse it as a stable key: it stays the same while the message grows.
roleGraineRolerequiredWho said it: USER or AGENT (GraineRole).
textStringrequiredWhat was said; an agent's message may be only a component.
atInstantrequiredWhen the message began; show it in the device's time zone.
deliveryGraineDelivery?nullWhere a user's message is (GraineDelivery); null for the agent's.
proactiveBooleanfalseUse it to badge a closed chat: the agent spoke up without being asked.
componentGraineComponent?nullThe component the agent drew with this message, when the platform ties one to it (GraineComponent).
liveBooleanfalseDraw the words as provisional while true: the agent's message is still arriving.
restoredBooleanfalsetrue for a message from an earlier conversation, restored when the chat opened: draw it as history, not as said just now.
channelGraineChatChannelGraineChatChannel.CHATWhere it was said (GraineChatChannel): typed, or on a call — only a restored message can be from a call.

GraineDelivery

Read it to show where one of the user's messages is.

enum class GraineDelivery { SENDING, SENT, FAILED }

What to expect — Only on the user's messages; the agent's are null.

CaseMeaning
SENDINGWaiting for the connection: typed while the chat was not connected, sent as soon as it is. Draw it as pending.
SENTOn its way to the agent. Draw it as sent.
FAILEDThe chat could not reconnect to send it. Offer a retry: GraineChat.retry().

GraineChatChannel

Read it to tell a typed message from one said on a call.

enum class GraineChatChannel(val wire: String)

What to expect — A chat window draws a run of VOICE messages as one “Voice call” row, never their words.

PropertyTypeMeaning
wireStringThe word Graine uses: chat or voice — for logs.
CaseMeaning
CHATTyped in the chat.
VOICESaid on a call with the agent — only a restored message is.

GraineChatStatus

Read it to show the chat's connection.

enum class GraineChatStatus { IDLE, CONNECTING, ONLINE, RECONNECTING, OFFLINE }

What to expect — IDLE → CONNECTING → ONLINE; a drop — or a first try that failed — goes to RECONNECTING and back; OFFLINE when it could not reconnect or the conversation ended.

CaseMeaning
IDLEThe chat is not in use — never opened, left by the user, or the app is in the background. Show nothing.
CONNECTINGBeing connected. Show “Connecting…”.
ONLINEConnected: messages go out as they are typed. Show “Online”.
RECONNECTINGThe chat is being connected again — after a drop, or after a first try that failed. Messages wait.
OFFLINEReconnecting failed, or the conversation was ended from the agent's side. A message, or retry(), connects again.

GraineChatSize

Pass it to choose how big the chat window opens.

enum class GraineChatSize(val wire: String)

What to expect — Which size wins: the size passed to chat.open(size), then GraineConfig.chatSize, then the agent's Window size in the dashboard, else compact. On a phone, large is a sheet over most of the screen and full the whole screen; on a tablet both open a panel in the corner.

PropertyTypeMeaning
wireStringThe design's word for it — compact, large or full — for logs.
CaseMeaning
COMPACTThe window above the voice bar.
LARGEOn a phone, a sheet covering most of the screen; on a tablet, a panel in the corner.
FULLThe whole screen.

GraineRole

Read it to know who said a line.

enum class GraineRole { USER, AGENT }

CaseMeaning
USERThe person using your app.
AGENTThe agent.

GraineChat (Compose)

Use it to show the SDK's own chat screen in a Compose Multiplatform UI.

@Composable
expect fun GraineChat(modifier: Modifier = Modifier, onClose: (() -> Unit)? = null, agent: String = Graine.DEFAULT_AGENT)

In ai.graine:inapp-kmp-compose. The chat as a whole screen, drawn in the agent's design: the header, one bubble per reply, the components the agent draws, the composer — and, where Graine rings this phone, the user's booked calls with Book a call (Booking with the SDK's sheet). It connects when it appears and hangs up shortly after it leaves the screen. A conversation that continues opens on its earlier messages — under day separators, an earlier call as one “Voice call” row, no starters — with New conversation in the header. On Android it hosts the Android SDK's chat view; on iOS the iOS SDK's chat screen, inside Compose's accessibility tree so VoiceOver reaches it. Nothing is drawn before Graine.configure, or for an agent that is not configured.

ParameterTypeDefaultMeaning
modifierModifierModifierWhere and how big the chat is drawn.
onClose(() -> Unit)?nullCalled when the user taps the header's close control. null: on iOS no close control is drawn and your navigation closes it.
agentStringGraine.DEFAULT_AGENTWhich configured agent's chat to show.

Returns — nothing (a composable).

Errors — none.

@Composable
fun ChatScreen(onBack: () -> Unit) {
    GraineChat(Modifier.fillMaxSize(), onClose = onBack)   // onBack: your navigation's back
}
 
@Composable
fun HelpScreen() {
    GraineChat(Modifier.fillMaxSize(), agent = "support")
}

GraineViews

Use it on iOS to present the SDK's own screens as UIKit view controllers.

object GraineViews

In iosMain. For an app that presents UIKit itself; Compose Multiplatform apps use GraineChat(), GraineCallScreen() and GraineAgentBar(), which host these.

What to expect — Each function throws IllegalStateException before Graine.configure (or for an agent that is not configured).

Its functions: chat, voiceBar, callScreen — each with its parameters, what it returns, its errors and an example.

GraineViews.chat

Call it on iOS to get an agent's chat as a view controller to present.

fun chat(onClose: (() -> Unit)? = null, agent: String = Graine.DEFAULT_AGENT): UIViewController

The iOS SDK's chat screen, drawn in the agent's design, with the user's booked calls and Book a call where Graine rings this phone.

ParameterTypeDefaultMeaning
onClose(() -> Unit)?nullCalled when the user taps the header's close control; null draws none — your navigation closes it.
agentStringGraine.DEFAULT_AGENTWhich configured agent's chat.

Returns — UIViewController.

Errors — IllegalStateException when the agent is not configured.

import platform.UIKit.UIViewController
 
fun showChat(from: UIViewController) {
    val chat = GraineViews.chat(onClose = { from.dismissViewControllerAnimated(true, completion = null) })
    from.presentViewController(chat, animated = true, completion = null)
}

GraineChatRows

Use it to lay out a chat screen of your own the way the SDK's chat window does.

object GraineChatRows

The rule every Graine SDK draws its own chat window by: a bubble per typed message; one “Voice call · 6:05 PM” row for a run of voice messages on one day; and a day separator — Today, Yesterday, a date — before the first row when it is not from today, and wherever the device's day changes between rows. A conversation all from today draws exactly as before.

Its functions: build, dayLabel, callLabel, offersStarters, offersNewConversation — each with its parameters, what it returns, its errors and an example.

GraineChatRows.build

Call it to turn the chat's messages into the rows to draw, in order.

fun build(turns: List<GraineChatTurn>, now: Instant = Clock.System.now(), zone: TimeZone = TimeZone.currentSystemDefault()): List<GraineChatRow>
ParameterTypeDefaultMeaning
turnsList<GraineChatTurn>requiredThe messages, oldest first — Graine.chat.turns.value.
nowInstantClock.System.now()The moment “today” is counted from; now when left out.
zoneTimeZoneTimeZone.currentSystemDefault()The time zone whose days separate the rows; the device's when left out.

Returns — List<GraineChatRow> (GraineChatRow). Example value: [Day(TODAY, 2026-10-15, …), Turn(turn, 0), VoiceCall("voice-t2", …, 1, 3)].

Errors — none.

val rows = GraineChatRows.build(Graine.chat.turns.value)

GraineChatRows.dayLabel

Call it to write a day separator's words.

fun dayLabel(row: GraineChatRow.Day, now: Instant = Clock.System.now(), zone: TimeZone = TimeZone.currentSystemDefault()): String
ParameterTypeDefaultMeaning
rowGraineChatRow.DayrequiredThe separator to label.
nowInstantClock.System.now()The moment “today” is counted from.
zoneTimeZoneTimeZone.currentSystemDefault()The time zone of the days.

Returns — String — “Today”, “Yesterday”, else the date, “Sat 10 Oct”, with the year when it is not this one (“Wed 31 Dec 2025”). Example value: "Yesterday".

Errors — none.

val separators = GraineChatRows.build(Graine.chat.turns.value)
    .filterIsInstance<GraineChatRow.Day>()
    .map { GraineChatRows.dayLabel(it) }   // ["Yesterday", "Today"]

GraineChatRows.callLabel

Call it to write an earlier call's row.

fun callLabel(clock: String): String
ParameterTypeDefaultMeaning
clockStringrequiredWhen the call's first message was said, in the device's short time format — “6:05 PM”.

Returns — String — “Voice call · 6:05 PM”. Example value: "Voice call · 6:05 PM".

Errors — none.

val words = GraineChatRows.callLabel("6:05 PM")

GraineChatRows.offersStarters

Call it to decide whether to show the brand's conversation starters.

fun offersStarters(turns: List<GraineChatTurn>): Boolean

Starters appear only in a fresh chat: they go once the user says something, and a continued conversation never shows them.

ParameterTypeDefaultMeaning
turnsList<GraineChatTurn>requiredThe messages, oldest first.

Returns — Boolean. Example value: true.

Errors — none.

val showStarters = GraineChatRows.offersStarters(Graine.chat.turns.value)

GraineChatRows.offersNewConversation

Call it to decide whether to offer “New conversation” in your chat's header.

fun offersNewConversation(turns: List<GraineChatTurn>): Boolean

Offered while restored messages are on screen — a conversation that continues.

ParameterTypeDefaultMeaning
turnsList<GraineChatTurn>requiredThe messages, oldest first.

Returns — Boolean. Example value: false.

Errors — none.

val offer = GraineChatRows.offersNewConversation(Graine.chat.turns.value)

GraineChatRow

Read it to draw one row of a chat laid out by GraineChatRows.build.

sealed class GraineChatRow

What to expect — Draw Day as a separator, VoiceCall as one row that a call happened (never its words) and Turn as a bubble.

PropertyTypeMeaning
abstract val key: StringStringUse it as the row's stable key while the conversation grows: a separator by its date, a call by its first message, a bubble by its message.
CaseMeaning
Day(day: GraineChatDay, date: LocalDate, at: Instant)A day separator: which day (GraineChatDay), the device's date, and the first message's time on it. Label it with GraineChatRows.dayLabel.
VoiceCall(id: String, at: Instant, first: Int, last: Int)An earlier call as one row: when its first message was said, and which messages (first to last, positions in the conversation) it stands for. Label it with GraineChatRows.callLabel.
Turn(turn: GraineChatTurn, index: Int)A message bubble: the message, and its position in the conversation.

GraineChatDay

Read it to know which day a separator names.

enum class GraineChatDay { TODAY, YESTERDAY, DATE }

CaseMeaning
TODAYToday, in the device's time zone.
YESTERDAYYesterday.
DATEAny earlier day: show the date.

The voice bar

The agent talking inside your app, beside the screen it talks about. Show the bar with GraineAgentBar() (Compose) or GraineViews.voiceBar() (UIKit); it places and ends the call itself, draws captions, the connection's state and the chat beside the call, all in the agent's design. Graine.voice is for an app that draws its own controls. The device asks the user for the microphone when the first call starts.

GraineVoice

Use it to start and end the voice bar's call from controls of your own.

class GraineVoice

Graine.voice, or GraineAgent.voice for another agent.

PropertyTypeMeaning
val state: StateFlow<GraineVoiceInfo>StateFlow<GraineVoiceInfo>Collect it to draw the call's state, what has been said and the captions switch (GraineVoiceInfo).

Its functions: start, end, setMuted, setCaptionsOn, clearCaptions — each with its parameters, what it returns, its errors and an example.

GraineVoice.start

Call it from your own “Talk” control to place the voice bar's call.

fun start()

Mounts the voice engine first if nothing has (on Android, in the activity in front). One conversation at a time: it ends the chat, and it cannot start while a native call is live.

Parameters — none.

Returns — nothing.

Errors — IllegalStateException when the agent is not configured. A refused microphone is the event GraineEvent.MicrophoneDenied.

Graine.voice.start()

GraineVoice.end

Call it from your own hang-up control.

fun end()

Parameters — none.

Returns — nothing.

Errors — IllegalStateException when the agent is not configured.

Graine.voice.end()

GraineVoice.setMuted

Call it from your own mute control.

fun setMuted(muted: Boolean)

Stops sending the user's voice without ending the call.

ParameterTypeDefaultMeaning
mutedBooleanrequiredtrue to stop sending the user's voice, false to send it again.

Returns — nothing.

Errors — IllegalStateException when the agent is not configured.

Graine.voice.setMuted(true)

GraineVoice.setCaptionsOn

Call it from your own captions (CC) control.

fun setCaptionsOn(on: Boolean)

Moves the captions switch, GraineVoiceInfo.captionsOn. While it is off the SDK's voice bar hides the line being said; the call goes on, and what was said is still kept in captions. On by default. On iOS it needs GraineInApp of this version; with an older one the captions stay on.

ParameterTypeDefaultMeaning
onBooleanrequiredtrue to show the captions, false to hide them.

Returns — nothing.

Errors — IllegalStateException when the agent is not configured.

Graine.voice.setCaptionsOn(!Graine.voice.state.value.captionsOn)

GraineVoice.clearCaptions

Call it to forget what has been said on the call — a fresh transcript for your own captions view.

fun clearCaptions()

Empties GraineVoiceInfo.captions without ending the call; the next line starts the list again. On iOS it needs GraineInApp of this version; an older one does nothing.

Parameters — none.

Returns — nothing.

Errors — IllegalStateException when the agent is not configured.

Graine.voice.clearCaptions()

GraineVoiceInfo

Read it to draw the voice bar's call in controls of your own.

data class GraineVoiceInfo(val connected: Boolean = false, val connecting: Boolean = false, val muted: Boolean = false, val captionsOn: Boolean = true, val captions: List<GraineCaption> = emptyList())

FieldTypeDefaultMeaning
connectedBooleanfalseShow the call as live: the agent and the user can hear each other.
connectingBooleanfalseShow “Connecting…”: the call is being placed.
mutedBooleanfalseShow the mute control as on: nothing the user says is sent.
captionsOnBooleantrueShow your captions (CC) control as on: the SDK's bar shows the line being said only while it is. On by default — a caption nobody asked to hide is an accessibility default. setCaptionsOn moves it.
captionsList<GraineCaption>emptyList()What has been said this call, oldest first: the last 12 lines, a live one replaced as its words arrive (GraineCaption). A new call starts empty. Kept whether the captions are on or off; clearCaptions() forgets them.

GraineCaption

Read it to show the line being said on a call.

data class GraineCaption(val role: GraineRole, val text: String, val live: Boolean)

The last line of a native call (GraineCallInfo.caption). The voice bar's lines arrive as the event GraineEvent.Caption and are kept, the last 12, in GraineVoiceInfo.captions; the captions switch (captionsOn) hides the bar's caption, never the list.

FieldTypeDefaultMeaning
roleGraineRolerequiredWho is speaking: the agent's words as they are spoken, the user's as they are heard.
textStringrequiredThe words so far.
liveBooleanrequiredDraw it as provisional while true: the line is still arriving.

GraineTransport

Pass it in GraineConfig.transport only to force how the voice bar's audio travels.

enum class GraineTransport(val wire: String)

What to expect — Leave transport null and the dashboard's Call audio setting decides.

PropertyTypeMeaning
wireStringThe setting's word: ws or rtc.
CaseMeaning
WEBSOCKETThe standard path: the audio on the conversation's own connection. Works everywhere.
WEBRTCHigh-quality audio, with a jitter buffer and the device's echo canceller; falls back to WEBSOCKET.

GraineAgentBar

Use it to show the voice bar in a Compose Multiplatform UI.

@Composable
expect fun GraineAgentBar(modifier: Modifier = Modifier, name: String? = null, onClose: (() -> Unit)? = null, agent: String = Graine.DEFAULT_AGENT)

In ai.graine:inapp-kmp-compose. It takes up the whole space you give it and draws the bar along the bottom edge, so place it in a strip at the bottom of your layout. It mounts the voice engine itself; on iOS VoiceOver reaches into it. The chat opens at the size passed to Graine.chat.open(size); failing that, at GraineConfig.chatSize; failing that, at the dashboard's setting — large and full open over the app, as a sheet or the whole screen. Nothing is drawn before Graine.configure.

ParameterTypeDefaultMeaning
modifierModifierModifierThe area the bar is drawn in, at its bottom.
nameString?nullThe name beside the status; null shows the dashboard's heading.
onClose(() -> Unit)?nullCalled when the user hung up, or closed the bar with no call — hide the bar there.
agentStringGraine.DEFAULT_AGENTWhich configured agent the bar talks to.

Returns — nothing (a composable).

Errors — none.

@Composable
fun HomeScreen() {
    var showBar by remember { mutableStateOf(true) }
    Box(Modifier.fillMaxSize()) {
        AppContent()
        if (showBar) GraineAgentBar(Modifier.align(Alignment.BottomCenter).fillMaxWidth().height(180.dp), onClose = { showBar = false })
    }
}

GraineViews.voiceBar

Call it on iOS to get an agent's voice bar as a view controller.

fun voiceBar(name: String? = null, onClose: (() -> Unit)? = null, agent: String = Graine.DEFAULT_AGENT): UIViewController

The bar is drawn at the bottom of the view it fills.

ParameterTypeDefaultMeaning
nameString?nullThe name beside the status; null shows the dashboard's heading.
onClose(() -> Unit)?nullCalled when the user hung up, or closed the bar with no call.
agentStringGraine.DEFAULT_AGENTWhich configured agent the bar talks to.

Returns — UIViewController.

Errors — IllegalStateException when the agent is not configured.

import platform.UIKit.*   // UIViewController and its child-controller calls
 
fun showVoiceBar(parent: UIViewController) {
    lateinit var bar: UIViewController
    bar = GraineViews.voiceBar(onClose = {        // the user hung up, or closed the bar
        bar.view.removeFromSuperview()
        bar.removeFromParentViewController()
    })
    parent.addChildViewController(bar)
    bar.view.setFrame(parent.view.bounds)         // the bar draws along the bottom of this frame
    parent.view.addSubview(bar.view)
    bar.didMoveToParentViewController(parent)
}

Graine.onRequestPermissionsResult

Call it from an Android activity's onRequestPermissionsResult when that activity shows the voice bar or places calls.

fun Graine.onRequestPermissionsResult(requestCode: Int, grantResults: IntArray): Boolean

The SDK asks for the microphone from the activity in front; forwarding the answer lets the call go ahead once it is granted. In androidMain only.

ParameterTypeDefaultMeaning
requestCodeIntrequiredAs Android passes it to your activity.
grantResultsIntArrayrequiredAs Android passes them to your activity.

Returns — Boolean — true when the request was the SDK's (and handled). Example value: true.

Errors — none.

override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
    if (Graine.onRequestPermissionsResult(requestCode, grantResults)) return
    super.onRequestPermissionsResult(requestCode, permissions, grantResults)
}

Several agents in one app

A support agent in Help, another agent elsewhere — each with its own publishable key, configured under its own name. Each agent has its own settings, chat, voice bar, actions, and scheduling and memory requests; who the user is, the install ID, the memory choice, the screen and the phone's calls are the app's and shared. Across all agents, only one conversation runs at a time. Graine itself is the default agent; the views take an agent name (GraineChat(agent = "support"), GraineAgentBar(agent = …), GraineViews.chat(agent = …)), and Graine.calls.startCall(agent = …) calls a named one.

Graine.configure(GraineConfig(baseUrl = "https://www.graine.ai", publishableKey = "pk_live_support…"), name = "support")
val support = Graine.agent("support")!!
support.chat.open()
Graine.calls.startCall(reason = "billing", agent = "support")

Graine.agent (by name)

Call it to reach an agent you configured under a name.

fun agent(name: String = DEFAULT_AGENT): GraineAgent?
ParameterTypeDefaultMeaning
nameStringDEFAULT_AGENTThe name it was configured under; the default agent when left out.

Returns — GraineAgent? — the agent, or null when nothing is configured under that name. Example value: GraineAgent(name="support").

Errors — none.

val support = Graine.agent("support") ?: return

GraineAgent

Use it to work with one particular agent — its chat, voice bar, actions, bookings and memory.

class GraineAgent

Returned by Graine.configure and Graine.agent. Graine is the default agent's; this is any agent's.

What to expect — Calling a function before this agent is configured throws IllegalStateException (“The agent "support" is not configured: call Graine.configure(config, name = "support") first.”).

PropertyTypeMeaning
val name: StringStringRead it to know which agent this is: the name it was configured under, "default" for the default agent.
val session: StateFlow<GraineSession?>StateFlow<GraineSession?>Collect it to know when this agent's settings have loaded; null before.
val theme: StateFlow<GraineThemeTokens?>StateFlow<GraineThemeTokens?>Collect it to draw UI of your own in this agent's design (GraineThemeTokens), for the phone's light or dark appearance — or the brand's fixed scheme — changing when the phone switches. null before its settings load, and when they carry no design. On iOS it needs GraineInApp of this version.
val events: SharedFlow<GraineEvent>SharedFlow<GraineEvent>Collect it to hear what this agent reports (GraineEvent). Not replayed.
val chat: GraineChatGraineChatUse it to drive this agent's chat.
val voice: GraineVoiceGraineVoiceUse it to drive this agent's voice-bar call.
val schedulingRules: GraineSchedulingRulesGraineSchedulingRulesRead it to know what this agent lets users book; the defaults (no booking) before its settings load.
val isConfigured: BooleanBooleanRead it to know whether this agent has been configured.
val bookingAvailable: BooleanBooleanRead it to decide whether to offer booking with this agent (GraineAgent.bookingAvailable).

Its functions: setVariables, currentVariables, registerAction, memories, forgetMemory, presentBooking, scheduleCall, scheduledCalls, cancelScheduledCall, startCall — each with its parameters, what it returns, its errors and an example.

Screen context, actions and components

Tell the agent what the user is looking at, and let it act in your app through handlers you register. The screen is the app's: every agent hears it while it has a live conversation. Values are masked on the device before anything leaves it; report real values. The agent's components (product lists, choices, time slots) are drawn by the SDK's views; an app that draws its own hears them as GraineEvent.Component.

Graine.setScreen

Call it on every meaningful change of what the user can see, so the agent talks about the right thing.

fun setScreen(screen: GraineScreen?)

An unchanged screen is not sent again; the first change goes at once and later ones at most every 400 ms. After a pause, the agent names a field that is INVALID or EMPTY instead of asking “need a hand?”. null clears it.

ParameterTypeDefaultMeaning
screenGraineScreen?requiredWhat the user can see now (GraineScreen), or null.

Returns — nothing.

Errors — IllegalStateException before configure.

val postalCode = "560001"   // what the user typed: the real value, masked on the device
Graine.setScreen(GraineScreen(
    screen = "checkout_address", title = "Delivery address",
    journey = GraineScreen.Journey("Checkout", step = 2, of = 3, next = "Payment"),
    fields = listOf(GraineField("postal_code", label = "Postal code", value = postalCode,
        status = GraineFieldStatus.INVALID, error = "We don't deliver there yet")),
))

Graine.updateField

Call it when one field changes, so a screen described once need not be described again on every keystroke.

fun updateField(field: GraineField)

Merged by name into the current screen (added when it is new) and sent like any screen change. Nothing happens before a screen is set.

ParameterTypeDefaultMeaning
fieldGraineFieldrequiredThe field as it is now (GraineField).

Returns — nothing.

Errors — IllegalStateException before configure.

val postalCode = "560034"   // what the user typed now
Graine.updateField(GraineField("postal_code", value = postalCode, status = GraineFieldStatus.FILLED))

Graine.removeField

Call it when a field leaves the screen.

fun removeField(name: String)

Nothing happens when it was never reported.

ParameterTypeDefaultMeaning
nameStringrequiredThe field's name.

Returns — nothing.

Errors — IllegalStateException before configure.

Graine.removeField("coupon")

Graine.clearScreen

Call it when the user leaves a screen — in the screen's teardown.

fun clearScreen(screenId: String? = null)

With screenId, only if that screen is still the one described — a screen closing late must not erase the one that replaced it. null clears whatever is there.

ParameterTypeDefaultMeaning
screenIdString?nullThe screen's screen ID, to clear only that one.

Returns — nothing.

Errors — IllegalStateException before configure.

Graine.clearScreen("checkout_address")

Graine.track

Call it to tell the agent about something the user did that informs without interrupting — a coupon applied, a filter changed.

fun track(name: String, data: Map<String, Any?> = emptyMap())

It rides along with the next screen update, among the user's recent events (the last ten are kept). Blank names are ignored. Masked on the device.

ParameterTypeDefaultMeaning
nameStringrequiredThe event's name, such as "coupon_applied".
dataMap<String, Any?>emptyMap()Details of the event, by name.

Returns — nothing.

Errors — IllegalStateException before configure.

Graine.track("coupon_applied", mapOf("code_length" to 8))

Graine.tap

Call it to tell the agent the user tapped a control the SDK cannot name by itself — a custom-drawn button, say.

fun tap(name: String)

Reported as tap:<name> among the screen's recent events, at most twice a second, and only while a screen is set. Labelled controls are noticed by themselves (GraineConfig.captureTaps).

ParameterTypeDefaultMeaning
nameStringrequiredThe control's name as the agent should read it; at most 40 characters are kept.

Returns — nothing.

Errors — IllegalStateException before configure.

Graine.tap("Apply coupon")

Graine.noteInteraction

Call it when the user does something that is not a field edit — scrolls a list, opens a picker — so the SDK does not think they are stuck.

fun noteInteraction()

Restarts the clock that notices a user stuck on a step. Field edits do not count: someone typing one character every forty seconds is exactly who is stuck.

Parameters — none.

Returns — nothing.

Errors — IllegalStateException before configure.

Graine.noteInteraction()

Graine.setVariables

Call it when your app learns something the default agent's prompt uses — a plan, a cart total.

fun setVariables(values: Map<String, Any?>?)

Merged over what is there: null under a key removes it, null for the whole map clears them all. Masked on the device; reaches the next conversation. GraineAgent.setVariables sets another agent's.

ParameterTypeDefaultMeaning
valuesMap<String, Any?>?requiredPrompt variables by name; numbers and booleans are written out as text, nested maps and lists are dropped.

Returns — nothing.

Errors — IllegalStateException before configure.

Graine.setVariables(mapOf("plan" to "gold", "cart_total" to 42))

GraineAgent.setVariables

Call it to set prompt variables for one of your other agents.

fun setVariables(values: Map<String, Any?>?)

Graine.setVariables for this agent.

ParameterTypeDefaultMeaning
valuesMap<String, Any?>?requiredPrompt variables by name; null under a key removes it, null clears all.

Returns — nothing.

Errors — IllegalStateException when this agent is not configured.

Graine.agent("support")?.setVariables(mapOf("ticket_count" to 2))

Graine.currentVariables

Call it to see exactly which prompt variables the default agent's next conversation will carry — on a debug screen, or before a support request.

fun currentVariables(): Map<String, String>

Every variable the agent will receive, merged in precedence order: the dashboard's defaults, GraineConfig.variables, setVariables, then who is signed in (identify's traits, the identity trait itself withheld). What the next conversation carries, as text; values are masked as they leave the device. A copy: changing it changes nothing. GraineAgent.currentVariables() reads another agent's. On iOS it needs GraineInApp of this version; an older one answers an empty map.

Parameters — none.

Returns — Map<String, String>. Example value: {plan=gold, cart_total=42, name=Asha}.

Errors — IllegalStateException before configure.

val vars = Graine.currentVariables()   // {plan=gold, cart_total=42, name=Asha}

GraineAgent.currentVariables

Call it to see the prompt variables one of your other agents will receive.

fun currentVariables(): Map<String, String>

Graine.currentVariables() for this agent: its dashboard defaults, its configuration's variables, its setVariables, then who is signed in.

Parameters — none.

Returns — Map<String, String> — a copy.

Errors — IllegalStateException when this agent is not configured.

val vars = Graine.agent("support")?.currentVariables().orEmpty()

Graine.registerAction

Call it to let the default agent do one thing in your app — open a screen, apply a filter, change a setting.

fun registerAction(name: String, handler: suspend (args: Map<String, Any?>) -> GraineActionResult): () -> Unit

Declare the action on the agent in the dashboard too: a handler with no declaration is never offered, and a declared action with no handler is refused when the agent asks for it. The handler starts on the main thread (it will usually touch your UI) and may suspend — a network call, a confirmation the user taps — continuing on the main thread. It must answer within six seconds, or the agent is told it was not done. A handler that throws answers error with its message; one cancelled answers error with “The action was cancelled.”. Defining an action again under the same name replaces its handler. GraineAgent.registerAction registers with another agent.

ParameterTypeDefaultMeaning
nameStringrequiredThe action's name, exactly as it is declared on the agent.
handlersuspend (args: Map<String, Any?>) -> GraineActionResultrequiredRuns the action with the agent's arguments and says how it went (GraineActionResult). Tell the truth: the agent speaks about exactly this.

Returns — () -> Unit — call it to unregister this handler.

Errors — IllegalStateException before configure.

val unregister = Graine.registerAction("open_screen") { args ->
    val screen = args["screen"] as? String ?: return@registerAction GraineActionResult.refused("No screen was named.")
    router.open(screen)
    GraineActionResult.ok("Opened $screen.")
}

GraineAgent.registerAction

Call it to give one of your other agents an action.

fun registerAction(name: String, handler: suspend (args: Map<String, Any?>) -> GraineActionResult): () -> Unit

Graine.registerAction for this agent: the same six seconds, the same rules.

ParameterTypeDefaultMeaning
nameStringrequiredThe action's name, as declared on this agent.
handlersuspend (args: Map<String, Any?>) -> GraineActionResultrequiredRuns the action and says how it went.

Returns — () -> Unit — call it to unregister.

Errors — IllegalStateException when this agent is not configured.

Graine.agent("support")?.registerAction("open_ticket") { args -> openTicket(args["id"] as String); GraineActionResult.ok() }

GraineScreen

Pass it to Graine.setScreen to describe what the user can see.

data class GraineScreen(…)

What to expect — Values are masked on the device before they leave it — report real ones; a screen that pre-redacts only blinds the agent.

FieldTypeDefaultMeaning
screenStringrequiredA stable ID for the screen, such as "checkout_address"; keep it constant, since friction rules and clearScreen match on it.
titleString?nullThe screen's title as the user sees it — what the agent calls the screen.
journeyJourney?nullWhere the screen sits in a flow (GraineScreen.Journey), so the agent can say “step 2 of 3”.
fieldsList<GraineField>emptyList()The inputs on the screen (GraineField): what lets the agent name the field that is wrong.
errorString?nullA screen-level error banner, as shown — distinct from a field's own.
dataMap<String, Any?>emptyMap()Anything else the agent should know about the screen: strings, numbers, booleans, and lists and maps of them.
availableActionsList<String>?nullWhich registered actions work on this screen: null allows every action this build registered, an empty list allows none.

GraineScreen.Journey

Pass it in GraineScreen.journey to say where a screen sits in a flow.

data class Journey(val name: String, val step: Int? = null, val of: Int? = null, val next: String? = null)

FieldTypeDefaultMeaning
nameStringrequiredThe flow's name, such as "Checkout".
stepInt?nullWhich step this screen is, counting from 1.
ofInt?nullHow many steps the flow has.
nextString?nullWhat comes next, such as "Payment" — so the agent can say what is left.

GraineField

Pass it in GraineScreen.fields (or to updateField) to describe one input.

data class GraineField(…)

What to expect — Its status is what lets the agent name the field that is wrong.

FieldTypeDefaultMeaning
nameStringrequiredA stable ID for the field, by which updateField and removeField find it.
labelString?nullThe label the user sees — what the agent calls the field.
valueString?nullThe real value; it is masked on the device on its way out. Never pre-redact it.
statusGraineFieldStatus?nullWhere the field stands (GraineFieldStatus).
errorString?nullThe validation message the user sees, so the agent can help them fix it.
optionalBooleanfalsetrue when leaving it blank is fine, so the agent does not count it as work left.

GraineFieldStatus

Use it to say where a field stands.

enum class GraineFieldStatus(val wire: String)

What to expect — INVALID and EMPTY are what make the agent speak up about a field.

PropertyTypeMeaning
wireStringThe word sent to the agent: empty, filled, invalid, pending or locked.
CaseMeaning
EMPTYNothing entered yet.
FILLEDEntered and accepted.
INVALIDEntered and refused — set error to the message shown.
PENDINGBeing checked (a code being verified, an upload in progress).
LOCKEDNot the user's to change — the agent does not ask them to.

GraineActionResult

Return it from an action handler to tell the agent what happened.

data class GraineActionResult(val status: Status, val message: String = "", val data: Map<String, Any?> = emptyMap())

What to expect — The agent speaks about exactly this, so tell the truth: ok only when it really happened.

FieldTypeDefaultMeaning
statusStatusrequiredHow it went (GraineActionResult.Status).
messageString""What the agent is told — not shown to the user; the agent decides how to say it.
dataMap<String, Any?>emptyMap()Structured details the agent can use in its reply.

Its functions: ok, refused, error — each with its parameters, what it returns, its errors and an example.

GraineActionResult.Status

Read it to know how an action went.

enum class Status(val wire: String) { OK("ok"), REFUSED("refused"), ERROR("error") }

PropertyTypeMeaning
wireStringThe word sent to the agent: ok, refused or error.
CaseMeaning
OKIt happened; the agent tells the user it is done.
REFUSEDThe action will not be done, with the reason — often the most helpful thing to tell the agent.
ERRORIt was tried and failed.

GraineActionResult.ok

Return it when the action happened.

fun ok(message: String = "", data: Map<String, Any?> = emptyMap()): GraineActionResult
ParameterTypeDefaultMeaning
messageString""What the agent is told happened.
dataMap<String, Any?>emptyMap()A structured result for the agent.

Returns — GraineActionResult with status OK.

Errors — none.

GraineActionResult.ok("The app is now in Spanish.")

GraineActionResult.refused

Return it when the action will not happen, and say why.

fun refused(message: String): GraineActionResult
ParameterTypeDefaultMeaning
messageStringrequiredWhy not, for the agent to explain.

Returns — GraineActionResult with status REFUSED.

Errors — none.

GraineActionResult.refused("There is no order with that number.")

GraineActionResult.error

Return it when the action was tried and failed.

fun error(message: String): GraineActionResult
ParameterTypeDefaultMeaning
messageStringrequiredWhat went wrong, for the agent.

Returns — GraineActionResult with status ERROR.

Errors — none.

GraineActionResult.error("The network is unavailable.")

GraineComponent

Read it to draw a component the agent sent, in an interface of your own.

data class GraineComponent(val id: String, val kind: String?, val title: String?, val payload: Map<String, Any?>)

Carried by GraineChatTurn.component and the event GraineEvent.Component. The SDK's own views draw components themselves; one you draw yourself is answered with Graine.chat.answerWidget (GraineChat.answerWidget).

FieldTypeDefaultMeaning
idStringrequiredThe component's ID: what an answer to it refers to.
kindString?requiredWhat it is — product_list, plan_comparison, choices, quick_actions, calendar_slots, info_card — or a newer kind; draw a fallback for one you do not know.
titleString?requiredIts title, when it has one.
payloadMap<String, Any?>requiredThe whole component as it arrived — items, choices, slots — to draw from.

GraineChat.answerWidget

Call it when the user answers a component the agent drew that you draw yourself — a choice tapped, a slot picked.

fun answerWidget(id: String, data: Map<String, Any?>, summary: String)

Sends the answer on the conversation that is live with that agent — the chat, else the voice bar's call. data is the structured answer, for your webhook; summary is the sentence that becomes the user's turn (“The blue one, please”) — without it the conversation does not move. The SDK's own views answer for themselves. On iOS it needs GraineInApp of this version; an older one does nothing.

ParameterTypeDefaultMeaning
idStringrequiredThe component's id (GraineComponent).
dataMap<String, Any?>requiredThe answer as data — strings, numbers, booleans, and lists and maps of them.
summaryStringrequiredThe answer in the user's words, said as their turn.

Returns — nothing.

Errors — IllegalStateException when the agent is not configured.

// Your own `choices` view calls this when the user taps one.
fun onChoiceTapped(component: GraineComponent, choice: String, label: String) {
    Graine.chat.answerWidget(component.id, mapOf("choice" to choice), summary = label)
}

Calls the user places

Calls like WhatsApp, on the phone's own call screen and over the lock screen, going on with the screen off: the user calls the agent with startCall; the agent can also ring the user (Calls Graine rings). Only one call can be live: a ring that arrives during a call is reported as missed, reason busy.

What a call does on each platform. Both platforms start a call at once on the agent's settings already kept on the device, and refresh them behind the call.

  • Android (the Android SDK's engine): opening the call has 15 seconds — “The call could not connect within 15 seconds.” Once connected, nothing at all arriving for 50 seconds (two 25-second keep-alive periods) ends the call: it ends ENDED with reason failed, and the SDK's call screen says “Call failed”. Sending never holds a call up on a slow network: at most 300 ms of the user's audio waits, the oldest dropped first. Hanging up lets go of the microphone at once, and a close the network cannot carry is forced within about a second.
  • iOS (the iOS SDK's engine, under CallKit): everything before the conversation opens — loading the agent, connecting — has one 20-second deadline: “The call could not connect within 20 seconds.” (the connection itself gives up after 15 seconds of trying to open: “The conversation did not open within 15 seconds.”). Once the conversation is open, the call's audio comes from CallKit; if it does not arrive, the SDK waits 3 seconds, starts the audio itself once, and waits 3 seconds more — then the call fails: “The call's audio could not start.”

A call that never connected reaches Graine.calls.outcomes as FAILED with these sentences in reason; the call screen says “Call failed”.

WebRTC. With the platform's WebRTC module in the app (WebRTC calls), calls in both directions carry their audio over WebRTC, as GraineCallsConfig.callTransport and the agent's In-app call audio setting allow. A call WebRTC cannot set up within about 12 seconds continues on the WebSocket inside the same call — “Connecting…” shows a moment longer — and Graine.calls.state says which one carried it (transport, transportFallbackReason, GraineCallInfo).

scope.launch {
    val call = Graine.calls.state.first { it.state == GraineCallState.ACTIVE }
    if (call.transportFallbackReason != null) println("Call ${call.callId} is on the WebSocket: ${call.transportFallbackReason}")
}

GraineCalls

Use it for native calls: configure them, place one, control it, and follow its state and outcomes.

class GraineCalls

Graine.calls — the app's one call engine, whichever agent a call is with. Its functions may be called from any thread.

What to expect — state always holds the latest value; outcomes and incoming are not replayed, so collect them from app start.

PropertyTypeMeaning
val state: StateFlow<GraineCallInfo>StateFlow<GraineCallInfo>Collect it to draw a call screen of your own, or to know when to show GraineCallScreen() (GraineCallInfo).
val outcomes: SharedFlow<GraineCallOutcome>SharedFlow<GraineCallOutcome>Collect it from app start to send each call's outcome to your server (GraineCallOutcome).
val incoming: SharedFlow<GraineIncomingCall>SharedFlow<GraineIncomingCall>Collect it to know when an agent's call starts ringing on this device (GraineIncomingCall).
val callPermissions: GraineCallPermissionsGraineCallPermissionsRead it for the call permissions the phone holds now (GraineCalls.callPermissions).
val callSetupProblems: List<GraineCallSetupProblem>List<GraineCallSetupProblem>Read it for what the app's setup still lacks for calls, each with its fix (GraineCalls.callSetupProblems).

Its functions: configure, startCall, end, setMuted, setSpeaker, answer, decline, callBack, dismissMissedCall, enableVoIPPushes, onNewToken, handlePush, requestCallPermissions, openCallSettings — each with its parameters, what it returns, its errors and an example.

GraineCalls.configure

Call it once at launch to set how calls ring and what they show.

fun configure(config: GraineCallsConfig)

On iOS, call it before enableVoIPPushes(). On Android it also creates the notification channels for rings, ongoing calls and missed calls.

ParameterTypeDefaultMeaning
configGraineCallsConfigrequiredThe calls' name, ring time, ringtone and icons (GraineCallsConfig).

Returns — nothing.

Errors — IllegalStateException before Graine.configure.

Graine.calls.configure(GraineCallsConfig(displayName = "Acme", ringTimeoutSeconds = 30))

GraineCallsConfig

Pass it to Graine.calls.configure to set how calls ring and what they show.

data class GraineCallsConfig(…)

What to expect — Set once at launch. Each field applies on the platforms its description names.

FieldTypeDefaultMeaning
displayNameString""The name a call rings with and shows when the ring names nobody; empty uses the agent's call name, else the app's own name.
ringTimeoutSecondsInt30How long a call from your own server rings before it is missed (on Android, held to 5–300 seconds). A call Graine rang rings for the agent's own ring time.
ringtoneString?nullThe ring: on Android a content URI, on iOS a sound file in the app's bundle; null uses the phone's ringtone.
smallIconInt0Android: a monochrome drawable resource for the status-bar icon; 0 uses the platform's call icon.
includeInRecentsBooleanfalseOn iOS, show these calls in the Phone app's Recents list.
callTransportGraineCallTransportGraineCallTransport.AUTOHow calls carry their audio, in both directions — calls the user places and calls Graine rings (GraineCallTransport): AUTO, WebRTC when the app includes the platform's WebRTC module, else the WebSocket; WEBSOCKET; or WEBRTC. The agent's In-app call audio setting can still move every call to the WebSocket.

GraineCallTransport

Pass it in GraineCallsConfig.callTransport to choose how native calls carry their audio; read it back in GraineCallInfo.transport.

enum class GraineCallTransport(val wire: String) { AUTO("auto"), WEBSOCKET("websocket"), WEBRTC("webrtc") }

It applies to calls in both directions: one the user places (startCall, callBack) and one Graine rang (a booked call, Ring now). WebRTC needs the platform's WebRTC module in the app (WebRTC calls). Mapped to each platform SDK's own setting.

What to expect — The agent's In-app call audio setting wins over the app: Standard keeps every call on the WebSocket, whatever callTransport says, from each app's next start. A call that tried WebRTC and could not use it before any audio flowed continues on the WebSocket inside the same call, and GraineCallInfo.transport says WEBSOCKET, with transportFallbackReason saying why. AUTO is only something you choose; a call's transport is WEBRTC or WEBSOCKET.

PropertyTypeMeaning
wireStringThe platform SDKs' word: auto, websocket or webrtc — for logs.
CaseMeaning
AUTOThe default: WebRTC when the app includes the WebRTC module and the dashboard allows it; else the WebSocket.
WEBSOCKETAlways the WebSocket — as a call's transport, the call's audio runs on the WebSocket.
WEBRTCWebRTC wherever it can carry the call; the WebSocket only when the module is missing or WebRTC cannot set up this call — as a call's transport, the call's audio runs on WebRTC.

Its functions: ofCall — each with its parameters, what it returns, its errors and an example.

GraineCalls.startCall

Call it from a “Call us” button to let the user call the agent.

fun startCall(reason: String? = null, variables: Map<String, Any?> = emptyMap(), agent: String? = null): String?

The call screen opens and the call connects. reason becomes the prompt's call_reason; variables are this call's own prompt variables, over the SDK's (masked on the device). It answers on the spot.

ParameterTypeDefaultMeaning
reasonString?nullWhy the user is calling — the agent hears it as call_reason. Blank is none.
variablesMap<String, Any?>emptyMap()Prompt variables for this call only.
agentString?nullWhich configured agent to call, by name; null for the default agent.

Returns — String? — the call's ID, which every outcome carries; null when another call is live or, on Android, when no screen of the app is in front to start it from. Example value: "call_6f1c2a9e-7d1b-4c47-9a6e-2f0d8b1c5e44".

Errors — IllegalStateException when the agent is not configured. A call that cannot connect is a FAILED outcome, not an exception.

val callId = Graine.calls.startCall(reason = "billing_help") ?: return showBusy()

GraineAgent.startCall

Call it to let the user call one of your other agents.

fun startCall(reason: String? = null, variables: Map<String, Any?> = emptyMap()): String?

A shortcut for Graine.calls.startCall(reason, variables, agent = name), with this agent's name filled in.

ParameterTypeDefaultMeaning
reasonString?nullWhy the user is calling — the agent's call_reason.
variablesMap<String, Any?>emptyMap()Prompt variables for this call only.

Returns — String? — the call's ID, or null when another call is live.

Errors — IllegalStateException when this agent is not configured.

Graine.agent("support")?.startCall(reason = "billing_help")

GraineCalls.end

Call it from your own hang-up control.

fun end()

Hangs up — or, while the call rings, declines it.

Parameters — none.

Returns — nothing.

Errors — none.

Graine.calls.end()

GraineCalls.setMuted

Call it from your own mute control.

fun setMuted(muted: Boolean)

Stops sending the user's voice without ending the call.

ParameterTypeDefaultMeaning
mutedBooleanrequiredtrue to stop sending the user's voice, false to send it again.

Returns — nothing.

Errors — none.

Graine.calls.setMuted(true)

GraineCalls.setSpeaker

Call it from your own loudspeaker control.

fun setSpeaker(on: Boolean)

Turning it off sends the audio back to a connected headset, or to the earpiece when there is none.

ParameterTypeDefaultMeaning
onBooleanrequiredtrue for the loudspeaker.

Returns — nothing.

Errors — none.

Graine.calls.setSpeaker(true)

GraineCallInfo

Read it to draw a call screen: the call's state, who is calling, the controls and the last line said.

data class GraineCallInfo(…)

Graine.calls.state.

What to expect — Every field has a value at every moment; with no call, state is IDLE and the rest are empty — except missedCall, kept until it is called back or dismissed.

FieldTypeDefaultMeaning
stateGraineCallStateGraineCallState.IDLERead it to know which moment to draw: ringing, connecting, on the call (GraineCallState).
callIdString?nullThe call's ID — the one its outcomes carry; null with no call.
callerNameString?nullThe name to show: the ring's caller name, else the agent's call name or the app's display name.
incomingBooleanfalsetrue when the agent rang (a push); false for a call the user placed.
hostedBooleanfalsetrue when Graine rang it — a call booked by the agent, the user or your team; the ring says “Scheduled call”.
mutedBooleanfalseShow the mute control as on.
speakerOnBooleanfalseShow the loudspeaker control as on (otherwise a headset, if one is connected, or the earpiece).
agentSpeakingBooleanfalseAnimate the agent's avatar while true: its voice is being heard.
captionGraineCaption?nullThe last line said, while a call is live (GraineCaption).
missedCallGraineMissedCall?nullThe last ring nobody answered, until it is called back or dismissed (GraineMissedCall) — show “Missed call” with Call back.
agentString?nullWhich agent the call is with, by the name it was configured under ("default" for the default agent); null with no call.
transportGraineCallTransport?nullWhat carries the call's audio, in either direction: WEBRTC or WEBSOCKET (GraineCallTransport). null with no call, before a transport is decided, and from a platform SDK older than WebRTC. Read it once the call is ACTIVE.
transportFallbackReasonString?nullWhy a call that tried WebRTC runs on the WebSocket — one of the values below; null for a call that did not fall back. Read it once the call is ACTIVE.
PropertyTypeMeaning
val showsCallScreen: BooleanBooleanRead it to decide whether to show a call screen: there is a call, or a missed ring to call back.

Why a call is on the WebSocket — the values of transportFallbackReason. Log them; the person sees nothing different, only “Connecting…” a moment longer.

ValueMeans
mint_failedThe call's WebRTC session could not be set up: refused, rate-limited or unreachable.
register_failedGraine's media service did not accept the connection, or never answered it.
no_responseThe call got no reply at all.
no_answerReplies came; the answer did not.
ice_timeoutAnswered, but the audio path never connected — usually a network that blocks WebRTC.
errorAnything else before the audio flowed.
dashboardAndroid only, for a call that would have used WebRTC: the agent's In-app call audio is Standard.
unavailableAndroid only, when the app chose WEBRTC: this build has no WebRTC module, or the device cannot run it.

GraineCallState

Read it to know where the one native call is.

enum class GraineCallState { IDLE, RINGING, CONNECTING, ACTIVE, ENDED }

What to expect — IDLE → RINGING (the agent rings) or CONNECTING (the user called) → ACTIVE → ENDED, then IDLE again once reported.

CaseMeaning
IDLENo call.
RINGINGThe agent is calling the user; show Answer and Decline.
CONNECTINGThe call was answered, or placed by the user, and is connecting.
ACTIVEThe agent and the user can hear each other.
ENDEDOver — reported once, then IDLE.

GraineCallOutcome

Read it to send what became of a call to your server.

data class GraineCallOutcome(…)

From Graine.calls.outcomes, as it happens.

What to expect — One call reports RINGING (an agent's call), then ANSWERED followed by ENDED or FAILED; or DECLINED, MISSED or FAILED alone. hosted means Graine rang it, and the SDK has already told Graine. Do not rely on its order relative to state.

FieldTypeDefaultMeaning
callIdStringrequiredUse it to match the outcome to the call: the push's call_id, Graine's for a ring it sent, or the ID startCall returned.
outcomeKindrequiredWhat became of it (GraineCallOutcome.Kind).
reasonString?nullWhy: for MISSED, no_answer or busy (another call was live); for ENDED, local (this app hung up), remote (the agent did) or failed (the connection was lost); for FAILED, the sentence saying why (at most 200 characters).
durationSecondsInt?nullHow long a call that connected lasted, in whole seconds — with ENDED.
hostedBooleanfalsetrue when Graine rang it — a call booked by the agent, the user or your team.
agentString?nullWhich agent the call was with, by the name it was configured under; null when the platform does not know (a ring refused as busy).

GraineCallOutcome.Kind

Read it to branch on what became of a call.

enum class Kind(val wire: String)

PropertyTypeMeaning
wireStringOne word for your server: ringing, answered, declined, missed, failed or ended.
CaseMeaning
RINGINGAn agent's call is ringing on this device.
ANSWEREDThe user picked up; ENDED or FAILED follows.
DECLINEDThe user declined it.
MISSEDThe ring went unanswered (no_answer), or the user was already on another call (busy).
FAILEDIt never connected; reason says why.
ENDEDA call that connected is over; reason says who ended it and durationSeconds how long it lasted.

GraineCallScreen

Use it to show the SDK's call screen in a Compose Multiplatform UI while there is a call.

@Composable
expect fun GraineCallScreen(modifier: Modifier = Modifier)

In ai.graine:inapp-kmp-compose. The ring (Answer, Decline — “Scheduled call” for a call Graine rang), connecting, the call with its controls, and a missed ring with Call back, in the agent's design. Show it whenever Graine.calls.state.value.showsCallScreen is true. On iOS it is drawn in place, and VoiceOver reaches into it. On Android the SDK's call screen is a full-screen activity that also covers the lock screen and opens by itself when a call rings or is placed; composing this brings it to the front.

ParameterTypeDefaultMeaning
modifierModifierModifierWhere the call screen is drawn (iOS).

Returns — nothing (a composable).

Errors — none.

val call by Graine.calls.state.collectAsState()
if (call.showsCallScreen) GraineCallScreen(Modifier.fillMaxSize())

GraineViews.callScreen

Call it on iOS to get the call screen as a view controller, for while the app is open.

fun callScreen(): UIViewController

The ring, the call, and a missed ring with Call back — the app's, whichever agent calls.

Parameters — none.

Returns — UIViewController.

Errors — IllegalStateException before Graine.configure.

import platform.UIKit.UIViewController
 
fun showCallScreen(from: UIViewController) {
    from.presentViewController(GraineViews.callScreen(), animated = true, completion = null)
}

Calls Graine rings

Graine rings your users itself — a call the agent booked in a conversation, one the user booked, or one your team booked — once your push credentials are uploaded (Workspace settings → In-app calling). On iOS, call enableVoIPPushes() at every launch; on Android, add Firebase Cloud Messaging to the app and the SDK registers for pushes itself (an app with a messaging service of its own forwards its token and data messages to onNewToken and handlePush). The SDK registers the device, rings each call on the phone's own call screen (“Scheduled call”), and reports what became of it. Your own server can ring the same way, with a push of type agent_call.

A call the person answers carries its audio exactly as one they place: over WebRTC when the app includes the platform's WebRTC module and GraineCallsConfig.callTransport and the agent's In-app call audio setting allow it (WebRTC calls) — prepared while the ring shows, so answering connects as fast as before — and Graine.calls.state reports transport and transportFallbackReason for it too.

The push your server sends (data only, nothing personal): type: "agent_call", call_id (required — every outcome carries it back), reason (the prompt's call_reason), caller_name (what the ring shows), variables (a JSON object as a string: more prompt variables), and agent_id (which of the app's agents the call is with).

GraineCalls.enableVoIPPushes

Call it at every launch on iOS, so the agent's calls can ring a closed app.

fun enableVoIPPushes()

iOS wakes a closed app for a VoIP push and hands it over once this has run; the SDK registers the token with Graine when Graine rings this app. Call configure first. Android: does nothing — with Firebase in the app the SDK registers for pushes itself (see onNewToken and handlePush for an app with its own messaging service).

Parameters — none.

Returns — nothing.

Errors — IllegalStateException before Graine.configure.

Graine.calls.enableVoIPPushes()

GraineCalls.onNewToken

Call it on Android from a FirebaseMessagingService of your own, in onNewToken. Without one, the SDK's own messaging service receives each new token, and the SDK reads the current one when it starts.

fun onNewToken(token: String)

The token is kept and registered with Graine when Graine rings this app. It works before Graine.configure, as a messaging service may start first. iOS: does nothing — the VoIP token comes through enableVoIPPushes().

ParameterTypeDefaultMeaning
tokenStringrequiredThe new Firebase Cloud Messaging token.

Returns — nothing.

Errors — none.

override fun onNewToken(token: String) = Graine.calls.onNewToken(token)

GraineCalls.handlePush

Call it on Android first thing in the onMessageReceived of a FirebaseMessagingService of your own. Without one, the SDK's own messaging service calls it for every message.

fun handlePush(data: Map<String, String>): Boolean

When the message is a call — Graine's own ring, or one your server sent — the SDK rings it and the answer is true; your own messages answer false. iOS: always false — a VoIP push reaches the SDK by itself.

ParameterTypeDefaultMeaning
dataMap<String, String>requiredThe message's data, exactly as the messaging service received it.

Returns — Boolean — true when it was a call and you should do nothing more with it. Example value: true.

Errors — none.

override fun onMessageReceived(message: RemoteMessage) {
    if (Graine.calls.handlePush(message.data)) return
    // …your own messages
}

GraineCalls.answer

Call it from a call screen of your own to pick up the ringing call.

fun answer()

The SDK's own call screen does this itself.

Parameters — none.

Returns — nothing.

Errors — none.

Graine.calls.answer()

GraineCalls.decline

Call it from a call screen of your own to decline the ringing call.

fun decline()

Reported as DECLINED. For a call Graine rang, nothing rings that user again for 6 hours.

Parameters — none.

Returns — nothing.

Errors — none.

Graine.calls.decline()

GraineCalls.callBack

Call it from a missed-call screen's “Call back”.

fun callBack(): String?

Returns the last missed call: the user calls the agent back, with the missed call's reason and its agent. On Android it starts from the screen in front.

Parameters — none.

Returns — String? — the new call's ID, or null when it could not start (another call is live, or no screen is in front). Example value: "call_8a20d3c1-…".

Errors — none.

Graine.calls.callBack()

GraineCalls.dismissMissedCall

Call it when the user has seen the missed call, so a call screen can go.

fun dismissMissedCall()

Clears GraineCallInfo.missedCall.

Parameters — none.

Returns — nothing.

Errors — none.

Graine.calls.dismissMissedCall()

GraineIncomingCall

Read it to know an agent's call started ringing on this device — for your own UI or analytics.

data class GraineIncomingCall(…)

From Graine.calls.incoming. The SDK shows the ring itself.

What to expect — It carries only what the ring shows: nothing personal.

FieldTypeDefaultMeaning
callIdStringrequiredThe call's ID, which its outcomes carry.
callerNameString?requiredRead it for the name on the ring: the push's caller name when it has one, otherwise the agent's call name or the app's display name.
reasonString?requiredWhy the agent is calling, when the push said; a call Graine rang keeps its reason with Graine.
hostedBooleanrequiredtrue when Graine rang it.
agentString?nullWhich configured agent is ringing; null when the platform does not say.

GraineMissedCall

Read it to show a missed call with “Call back”.

data class GraineMissedCall(…)

GraineCallInfo.missedCall: the last ring nobody answered, kept until callBack() or dismissMissedCall(). A ring refused because another call was live is not kept.

FieldTypeDefaultMeaning
callIdStringrequiredThe missed call's ID.
callerNameString?requiredWho called, as the ring showed it.
reasonString?requiredWhy, when the ring said.
hostedBooleanrequiredtrue when Graine rang it.
atInstantrequiredWhen it was missed; show it in the device's time zone.

Permissions and the setup check

What a call needs from the phone, and from your app's setup, in one place — read from the platform SDKs, the same on both. The SDK never asks on its own at launch: it asks for the microphone when a call starts without it, and requestCallPermissions() asks earlier, when your app chooses (on Android from a transparent screen of the SDK's own, over the app's screen in front). callSetupProblems says what your setup still lacks; the platform SDK logs each problem once from configure on. A problem is never an exception: calls still work where they can. On iOS these need GraineInApp of this version.

GraineCalls.callPermissions

Read it to see which call permissions the phone holds now.

val callPermissions: GraineCallPermissions

The microphone, and on Android the ring's notifications (13+ asks) and the full-screen ring (14+, Settings only); on iOS those two are NOT_NEEDED — CallKit rings and draws its own screen.

Returns — a GraineCallPermissions. Read it again when the app returns to the foreground: the user may have changed Settings.

Errors — IllegalStateException before Graine.configure.

if (!Graine.calls.callPermissions.allGranted) showFinishSetupCard()

GraineCalls.requestCallPermissions

Call it at a moment you choose — onboarding, a "Call me" screen — to ask for what a call needs.

suspend fun requestCallPermissions(): GraineCallPermissions

Asks for the call permissions the system's dialog can still ask for — the microphone, and notifications on Android 13+ — and returns them as they stand after; suspends until the user answers (at once when there is nothing to ask). The full-screen ring has no dialog: openCallSettings(GraineCallPermission.FULL_SCREEN) opens its page.

Returns — a GraineCallPermissions.

Errors — IllegalStateException before Graine.configure.

scope.launch {
    val p = Graine.calls.requestCallPermissions()
    if (p.fullScreen == GraineCallPermissionStatus.BLOCKED) Graine.calls.openCallSettings(GraineCallPermission.FULL_SCREEN)
}

GraineCalls.openCallSettings

Call it to send the user to the Settings page that changes a call permission.

fun openCallSettings(item: GraineCallPermission): Boolean

Opens, on Android, the full-screen ring's own page (14+) or the app's notification settings, from the screen in front; otherwise the app's page, where the microphone is.

ParameterTypeDefaultMeaning
itemGraineCallPermissionrequiredThe permission the user should change.

Returns — Boolean: true when a page opened.

Errors — IllegalStateException before Graine.configure.

Graine.calls.openCallSettings(GraineCallPermission.MICROPHONE)

GraineCalls.callSetupProblems

Read it to learn what your app's setup still lacks for calls, in plain words.

val callSetupProblems: List<GraineCallSetupProblem>

What the app's setup still lacks for calls, read now — each a stable code and a sentence with the exact fix; empty when nothing is missing. The codes: iOS microphone_usage_missing, background_audio_missing, background_voip_missing (Info.plist), push_entitlement_missing, voip_pushes_not_enabled (no enableVoIPPushes() 15 seconds after configure), no_voip_token; Android manifest_entry_missing, no_push_token (no push token 15 seconds after configure: Firebase missing or not set up in the app, or no onNewToken from an app with its own messaging service), notifications_not_allowed, full_screen_not_allowed; both microphone_blocked and hosted_ringing_off (no push credentials uploaded for this platform: Graine can't ring this phone).

Returns — List<GraineCallSetupProblem>.

Errors — IllegalStateException before Graine.configure.

Graine.calls.callSetupProblems.forEach { println("${it.code}: ${it.message}") }

GraineCallPermission

Use it to name a call permission.

enum class GraineCallPermission(val wire: String) { MICROPHONE("microphone"), NOTIFICATIONS("notifications"), FULL_SCREEN("fullScreen") }

What to expect — Three permissions, the same on every SDK. On iOS only the microphone is ever asked for.

PropertyTypeMeaning
wireStringThe word every Graine SDK uses for it.
CaseMeaning
MICROPHONEThe user's voice.
NOTIFICATIONSA call that rings while the app is closed (Android; 13+ asks). iOS rings through CallKit, which needs none.
FULL_SCREENA ring that fills a locked screen (Android 14+, allowed in Settings). iOS: CallKit's own screen.
companion object { fun of(word: String?): GraineCallPermission? }

GraineCallPermission.of(word) reads one by its word — microphone, notifications, fullScreen (full_screen and any case too); null for any other.

GraineCallPermissionStatus

Read it to tell whether a call permission is allowed, can still be asked for, or only Settings can change it.

enum class GraineCallPermissionStatus(val wire: String) { GRANTED("granted"), DENIED("denied"), BLOCKED("blocked"), NOT_NEEDED("notNeeded") }

What to expect — GRANTED and NOT_NEEDED need nothing; DENIED means requestCallPermissions() can ask; BLOCKED means only Settings can change it — offer openCallSettings.

PropertyTypeMeaning
wireStringThe word every Graine SDK uses for it.
isSatisfiedBooleanGranted, or nothing to grant.
CaseMeaning
GRANTEDAllowed.
DENIEDNot allowed yet: requestCallPermissions() can ask for it.
BLOCKEDRefused for good, or only allowed in Settings.
NOT_NEEDEDThis phone needs no such permission.
companion object { fun of(word: String?): GraineCallPermissionStatus }

GraineCallPermissionStatus.of(word) reads one from a platform's word; anything it does not know is DENIED, never granted.

GraineCallPermissions

Read it to see every call permission at once.

data class GraineCallPermissions(val microphone: GraineCallPermissionStatus, val notifications: GraineCallPermissionStatus, val fullScreen: GraineCallPermissionStatus)

What to expect — A snapshot: read callPermissions again for the latest. allGranted is the one check before offering calls; permissions[GraineCallPermission.MICROPHONE] reads one by name.

PropertyTypeMeaning
microphoneGraineCallPermissionStatusThe microphone.
notificationsGraineCallPermissionStatusThe ring's notifications — NOT_NEEDED on iOS.
fullScreenGraineCallPermissionStatusThe full-screen ring — NOT_NEEDED on iOS.
allGrantedBooleanEvery permission is granted or not needed: calls can ring and connect.

GraineCallSetupProblem

Read it to show or log what your app's setup still lacks.

data class GraineCallSetupProblem(val code: String, val message: String)

What to expect — The code is stable and the same on every SDK (the list is under GraineCalls.callSetupProblems); the message is a sentence for the developer, with the exact fix.

PropertyTypeMeaning
codeStringStable: act on it, or count it.
messageStringWhat is missing and how to fix it, in plain words.

Booking with the SDK's sheet

“Call me at…” with no screen of your own: presentBooking shows the native SDK's booking sheet in the agent's design, and Graine rings this phone at the time the user picks.

What the sheet shows. A header, “Book a call”, with “{call name} will call you” under it — the agent's call name, else its chat title, else its heading. Day: a chip for each day from today to the furthest day the agent allows — “Today”, “Tomorrow”, then the weekday and date, such as “Thu 16 Oct”. Time: Android's own time picker in spinner mode (inside the SDK's own dialog) or iOS's time wheel, both in 5-minute steps, held to the times the chosen day offers between earliest(now) (rounded up to the next 5 minutes) and latest(now). The phone's time zone and the chosen time, such as “Asia/Kolkata · Tue 14 Oct, 6:00 PM”. What's it about? (optional): one line, at most 120 characters. Book call, with a spinner while it books; then “Booked — {call name} will call you on {day}, {time}” and Done. A sheet up from the bottom on a phone, a centred panel on a tablet.

A refusal stays in the sheet as one line under the button:

CodeThe line
too_soonPick a time at least N minutes from now
too_farPick a time within N days
outside_windowThat time is outside the hours we can call — pick another
too_many_pendingYou already have N calls booked — cancel one first
scheduling_offBooking a call isn't available right now
identity_requiredSign in to book a call
anything elseCouldn't book just now — try again

N is the agent's own number, singular for one (“1 minute”). An agent that cannot be booked now opens a sheet that says why (the scheduling_off or identity_required line) and books nothing.

In the chat. The SDK's chat windows (GraineChat(), GraineViews.chat()) show the user's booked calls in their Upcoming calls card, with Book a call under them where booking is available — or alone in the card's place when none are booked; never when the design hides the booked-calls card. A booking refreshes the card.

Graine.bookingAvailable

Read it to decide whether to show your own “Call me” button.

val bookingAvailable: Boolean

true when Graine rings this phone (the workspace's In-app calling is on, for this platform), the default agent lets users book, and the user is identified with the trait the agent's Who is the user? setting names. The native SDK's own rule, read as it is now; false before configure, and on iOS with a GraineInApp older than this library (0.7.0). Check it again after identify and after the agent's settings have loaded. GraineAgent.bookingAvailable answers for another agent.

Parameters — none.

Returns — Boolean. Example value: true.

Errors — none.

val session by Graine.session.collectAsState()   // composes again when the settings load
if (session != null && Graine.bookingAvailable) {
    Button(onClick = { scope.launch { Graine.presentBooking() } }) { Text("Call me") }
}

Graine.presentBooking

Call it from your own “Call me” button to let the user book a call in the SDK's sheet.

suspend fun presentBooking(reason: String? = null): GraineScheduledCall?

Shows the booking sheet (above) for the default agent and returns once it closes. On Android it opens over the activity in front; on iOS over the key window's top view controller. One sheet at a time.

ParameterTypeDefaultMeaning
reasonString?nullFills What's it about?; the user may change it. Kept with the call for the agent and never put in the push that rings it.

Returns — GraineScheduledCall? — the booked call (GraineScheduledCall), or null when the user closed the sheet without booking, a sheet is already open, or (Android) no activity is in front. Example value: GraineScheduledCall(id="6f1c2a9e-…", at=2026-10-16T12:30:00Z, status=Scheduled, source=App, reason="Talk through my order").

Errors — IllegalStateException before configure. On iOS with a GraineInApp older than this library: GraineException Unavailable — “This app links a GraineInApp without the booking sheet. Link GraineInApp 0.8.0, the version of this library.”. Refusals stay in the sheet: it does not throw them.

scope.launch {
    if (!Graine.bookingAvailable) return@launch
    val call = Graine.presentBooking(reason = "Talk through my order") ?: return@launch
    showToast("We'll call you then.")
}

GraineAgent.bookingAvailable

Read it to decide whether to offer booking with one of your other agents.

val bookingAvailable: Boolean

Graine.bookingAvailable for this agent: Graine rings this phone, this agent lets users book, and the user is identified with this agent's identity trait. false before the agent is configured.

Parameters — none.

Returns — Boolean.

Errors — none.

val canBook = Graine.agent("support")?.bookingAvailable == true

GraineAgent.presentBooking

Call it to show the booking sheet for one of your other agents.

suspend fun presentBooking(reason: String? = null): GraineScheduledCall?

Graine.presentBooking for this agent, in its design and with its rules.

ParameterTypeDefaultMeaning
reasonString?nullFills What's it about?.

Returns — GraineScheduledCall? — the booked call, or null when nothing was booked.

Errors — IllegalStateException when this agent is not configured; on iOS with an older GraineInApp, GraineException Unavailable, as Graine.presentBooking.

val call = Graine.agent("support")?.presentBooking()

The scheduled-calls list

Book calls from your own interface, list the user's upcoming calls and cancel one — the same calls the sheet and the chat's card show. Graine rings this device at the booked time.

GraineSchedulingRules

Read it to build your own booking screen — which times the agent accepts, and whether users may book at all.

data class GraineSchedulingRules(…)

Graine.schedulingRules (or GraineAgent.schedulingRules, GraineSession.schedulingRules).

What to expect — From the agent's settings once they load; before that the defaults hold, with booking off. scheduleCall refuses on the device first — SchedulingOff, TooSoon (before earliest(now)), TooFar (after latest(now)) — and Graine then checks the calling hours and the calls already waiting.

FieldTypeDefaultMeaning
usersCanBookBooleanfalseRead it to know whether users may book calls from the app at all; false when the settings say nothing.
minLeadMinutesInt5The least notice a booking needs, in minutes — use it with earliest().
maxDaysAheadInt14How far ahead a booking may be, in days — use it with latest().
maxPendingPerUserInt3How many upcoming calls a user may have at once; beyond it, TooManyPending.
ringTimeoutSecondsInt45How long a call Graine rings keeps ringing before it is missed, 20–60 seconds.
callNameString?nullThe name the agent's calls ring with; null uses the app's display name.
hostedRingingBooleanfalseRead it to know whether Graine rings this workspace's users itself (its push credentials are uploaded).
platformsList<GrainePlatform>emptyList()The phones Graine rings (GrainePlatform); empty means every platform.

Its functions: earliest, latest — each with its parameters, what it returns, its errors and an example.

GraineSchedulingRules.earliest

Call it for the soonest time a booking is accepted, to start your time picker there.

fun earliest(now: Instant = Clock.System.now()): Instant
ParameterTypeDefaultMeaning
nowInstantClock.System.now()The moment to count from; now when left out.

Returns — Instant — now plus minLeadMinutes. Example value: 2026-10-15T12:05:00Z.

Errors — none.

val first = Graine.schedulingRules.earliest()

GraineSchedulingRules.latest

Call it for the latest time a booking is accepted, to end your time picker there.

fun latest(now: Instant = Clock.System.now()): Instant
ParameterTypeDefaultMeaning
nowInstantClock.System.now()The moment to count from; now when left out.

Returns — Instant — now plus maxDaysAhead days. Example value: 2026-10-29T12:00:00Z.

Errors — none.

val last = Graine.schedulingRules.latest()

GrainePlatform

Read it in GraineSchedulingRules.platforms to know which phones Graine rings.

enum class GrainePlatform(val wire: String) { IOS("ios"), ANDROID("android") }

PropertyTypeMeaning
wireStringThe platform's word: ios or android.
CaseMeaning
IOSiPhones and iPads.
ANDROIDAndroid phones and tablets.

Graine.scheduleCall

Call it to book a call from your own booking screen.

suspend fun scheduleCall(at: Instant, reason: String? = null): GraineScheduledCall

Books a call from the default agent for the signed-in user (or, with nobody signed in, this install) at at — “call me tomorrow at 6”; Graine rings this device then. It waits for the agent's settings (at most 20 seconds), then refuses on the device — before anything is sent — what the rules already settle, in the booking sheet's words: SchedulingOff, “Booking a call isn't available right now”; TooSoon, “Pick a time at least N minutes from now”; TooFar, “Pick a time within N days” (N singular for one: “1 minute”, “1 day”). Graine then checks its own: the calling hours, in this device's time zone, and the calls already waiting. reason is trimmed to 120 characters, kept with the call for the agent, and never put in the push that rings it. An open chat's Upcoming calls card shows the booking at once. GraineAgent.scheduleCall books with another agent.

ParameterTypeDefaultMeaning
atInstantrequiredWhen the call should ring: between schedulingRules.earliest() and latest().
reasonString?nullWhat the call is about, at most 120 characters — the agent hears it when it rings.

Returns — GraineScheduledCall — the booked call (GraineScheduledCall), status Scheduled. Example value: GraineScheduledCall(id="6f1c2a9e-…", at=2026-10-16T12:30:00Z, status=Scheduled, source=App, reason="Talk through my order").

Errors — throws GraineException — on the device: SchedulingOff, TooSoon, TooFar; from Graine: OutsideWindow, TooManyPending, IdentityRequired, DeviceLimit, InvalidRequest, RateLimited, Network, Unavailable (“The booking was accepted but not described.” when Graine booked it without describing it).

try {
    val call = Graine.scheduleCall(Clock.System.now() + 1.hours, reason = "Talk through my order")
    showToast("Booked for ${call.at}")
} catch (e: GraineException) {
    showMessage(e.message.orEmpty())
}

GraineAgent.scheduleCall

Call it to book a call with one of your other agents.

suspend fun scheduleCall(at: Instant, reason: String? = null): GraineScheduledCall

Graine.scheduleCall for this agent, checked against this agent's rules.

ParameterTypeDefaultMeaning
atInstantrequiredWhen the call should ring.
reasonString?nullWhat it is about, at most 120 characters.

Returns — GraineScheduledCall.

Errors — throws GraineException as Graine.scheduleCall.

val call = Graine.agent("support")?.scheduleCall(Clock.System.now() + 2.hours, reason = "Renewal questions")

Graine.scheduledCalls

Call it to list the user's upcoming calls in your own interface.

suspend fun scheduledCalls(): List<GraineScheduledCall>

The calls the default agent has booked for the signed-in user (or this install) that are still to come, soonest first.

Parameters — none.

Returns — List<GraineScheduledCall>. Example value: [GraineScheduledCall(id="6f1c2a9e-…", at=2026-10-16T12:30:00Z, status=Scheduled, source=Agent, reason=null)].

Errors — throws GraineException: Network, RateLimited, Unavailable, InvalidKey, OriginRefused.

val upcoming = Graine.scheduledCalls()

GraineAgent.scheduledCalls

Call it to list the calls one of your other agents has booked for the user.

suspend fun scheduledCalls(): List<GraineScheduledCall>

Parameters — none.

Returns — List<GraineScheduledCall>, soonest first.

Errors — throws GraineException as Graine.scheduledCalls.

val upcoming = Graine.agent("support")?.scheduledCalls().orEmpty()

Graine.cancelScheduledCall

Call it when the user calls off one of their booked calls.

suspend fun cancelScheduledCall(id: String): GraineCallStatus

Only a call booked for this user or install; any other is NotFound.

ParameterTypeDefaultMeaning
idStringrequiredThe call's id, from scheduleCall, scheduledCalls or presentBooking.

Returns — GraineCallStatus — its status afterwards, Cancelled. Example value: GraineCallStatus.Cancelled.

Errors — throws GraineException: InvalidRequest (“No call id.”) for a blank id, NotFound, Network, RateLimited, Unavailable.

Graine.cancelScheduledCall("6f1c2a9e-7d1b-4c47-9a6e-2f0d8b1c5e44")

GraineAgent.cancelScheduledCall

Call it to call off a call one of your other agents booked.

suspend fun cancelScheduledCall(id: String): GraineCallStatus
ParameterTypeDefaultMeaning
idStringrequiredThe call's id.

Returns — GraineCallStatus — Cancelled.

Errors — throws GraineException as Graine.cancelScheduledCall.

Graine.agent("support")?.cancelScheduledCall("6f1c2a9e-7d1b-4c47-9a6e-2f0d8b1c5e44")

GraineScheduledCall

Read it to show a booked call: when it rings, where it is, and whether it can still be cancelled.

data class GraineScheduledCall(…)

What to expect — From scheduleCall (status Scheduled), scheduledCalls and presentBooking. at is a moment: show it in the device's time zone. A status or source newer than the SDK arrives as Unknown, carrying its word.

FieldTypeDefaultMeaning
idStringrequiredUse it to cancel the call — Graine's ID for it, a UUID.
atInstant?requiredWhen it rings; null only when Graine's time could not be read.
statusGraineCallStatusrequiredWhere it is (GraineCallStatus).
sourceGraineCallSource?nullWho booked it (GraineCallSource); null when Graine does not say.
reasonString?nullWhat it is about, when Graine shares it — at most 120 characters.
PropertyTypeMeaning
val isUpcoming: BooleanBooleanUse it to keep a call in an upcoming list: true while it is Scheduled or Ringing.
val canCancel: BooleanBooleanUse it to show a Cancel control: true while it is Scheduled, before it rings.

GraineCallStatus

Read it to show where a booked call is.

sealed class GraineCallStatus(val wire: String)

What to expect — Scheduled → Ringing → Answered → Completed; or Declined, Missed, Failed, Cancelled, Expired, NoDevice. A status newer than this SDK arrives as Unknown(raw): give every when an else.

PropertyTypeMeaning
wireStringGraine's word for the status — scheduled, no_device, … — for logs and your server.
CaseMeaning
ScheduledBooked, waiting for its time; it can still be cancelled.
RingingRinging the user now.
AnsweredThe user picked up.
CompletedThe call took place and is over.
DeclinedThe user declined it; nothing rings them again for 6 hours.
MissedIt rang out unanswered.
FailedIt could not be placed.
CancelledCalled off before it rang.
ExpiredIts time passed while it could not ring — more than 10 minutes late, or no opening left in the calling hours.
NoDeviceNothing could be rung: the user has no device registered for calls in the last 30 days.
Unknown(raw)A status newer than this SDK, carrying Graine's word in raw.

Its functions: from — each with its parameters, what it returns, its errors and an example.

GraineCallSource

Read it to show who booked a call.

sealed class GraineCallSource(val wire: String)

What to expect — A source newer than this SDK arrives as Unknown(raw).

PropertyTypeMeaning
wireStringGraine's word for the source — sdk for App.
CaseMeaning
AgentThe agent booked it in a conversation.
AppYour app booked it (scheduleCall, the booking sheet).
DashboardYour team booked it in the dashboard.
ApiYour server booked it through the API.
Unknown(raw)A source newer than this SDK, carrying Graine's word.

Its functions: from — each with its parameters, what it returns, its errors and an example.

Design

The SDK's own screens — the chat, the voice bar, the call screen and the booking sheet — draw the design the brand chose in the Garage: colours, typefaces, corners, the launcher's look, light and dark. The last design is kept on the device, so a call that wakes a closed app is drawn in it too. An app that draws UI of its own in the brand's design reads the same tokens from shared code: Graine.theme (each agent's theme), a GraineThemeTokens.

Graine.theme

Collect it to draw UI of your own — a card, a button, a screen around the agent — in the brand's design.

val theme: StateFlow<GraineThemeTokens?>

The default agent's design tokens: the colours, type, corners, depth, motion and icons the SDK's own screens draw with, and the chat window's and the call screen's tokens, for the phone's current light or dark appearance — or the brand's fixed scheme. It changes when the phone switches between light and dark. null before the agent's settings load, and when they carry no design. Each agent has its own: GraineAgent.theme. On iOS it needs GraineInApp of this version; with an older one it stays null.

Parameters — none.

Returns — StateFlow<GraineThemeTokens?> (GraineThemeTokens). Example value: GraineThemeTokens(v=1, preset="graine-classic", scheme="light", colors=Colors(background="#F7F7F5", …), …).

Errors — none.

val t = Graine.theme.value ?: return   // null before the settings load, or without a design
val accent = t.colors.accent           // "#318CE7"
val corner = t.shape.radius.lg         // 18.0, in dp (points on iOS)
val heading = t.font.heading           // "DM Sans", or "system" for the platform's own font

GraineThemeTokens

Read it to draw UI of your own in the brand's design.

data class GraineThemeTokens(…)

One scheme's design tokens — the design contract every Graine SDK draws from, the same values as React Native's useGraineTheme(), iOS theme, Android themeTokens() and Flutter tokens — as plain numbers and #RRGGBB strings. Lengths are dp (points on iOS), type sizes sp. From Graine.theme and each agent's theme.

What to expect — Resolved by the native SDK from the agent's design when the settings load, for the phone's light or dark appearance (or the brand's fixed scheme); reading them never throws. A design without a complete set of colours is no design: theme is null. The words — "outline", "spring", "window" — are the design's own: compare against them, and keep a default branch, since a newer design can send a word this build does not know.

FieldTypeMeaning
vIntThe token format's version, for logs.
presetStringThe design style the brand started from — informational: the tokens are already resolved.
schemeStringWhich of the design's two sets this is: light or dark.
colorsColorsEvery colour a surface is drawn in (GraineThemeTokens.Colors).
fontFontThe typefaces, sizes, weights, letter spacing and line heights (GraineThemeTokens.Font).
shapeShapeCorner radii, outline width and the launcher's outline (GraineThemeTokens.Shape).
spacingSpacingThe spacing unit, card padding and gap (GraineThemeTokens.Spacing).
elevationElevationHow depth is drawn: soft shadows, or a hard offset (GraineThemeTokens.Elevation).
motionMotionHow long and how things move (GraineThemeTokens.Motion).
iconsIconsThe icons' style and stroke, and the launcher's glyph (GraineThemeTokens.Icons).
glassGlass?Translucent surfaces (GraineThemeTokens.Glass); null for opaque ones.
bubblesStringHow chat messages sit on the page: filled, outline or minimal.
chromeStringnone, or window: the chat, the caption and components open as desktop windows.
chatChat?The chat window's own tokens (GraineThemeTokens.Chat); null from a server that predates them — draw from colors.
callCall?The call screen's tokens (GraineThemeTokens.Call); null from a server that predates them.
PropertyTypeMeaning
val isDark: BooleanBooleanRead it to know these are the dark tokens (scheme == "dark").

GraineThemeTokens.Colors

Read it to colour your own UI exactly as the agent's design does. data class Colors(…), as GraineThemeTokens.colors: seventeen colours, each #RRGGBB.

FieldTypeMeaning
backgroundStringThe page behind everything in the chat window — your own full-screen backgrounds.
surfaceStringThe voice bar, the chat header and sheets — your own bars and panels.
surfaceRaisedStringCards, the message field and unselected chips — anything that sits on a surface.
textStringMain text.
textMutedStringSecondary text: captions, labels, times.
borderStringHairlines and outlines.
accentStringThe brand colour: primary buttons, the launcher, the send button, anything selected.
onAccentStringText and icons drawn on the accent.
accentSoftStringThe accent at low strength: chips, selected rows, the focus halo.
successStringGood news: online dots, confirmations.
warningStringCautions.
dangerStringErrors, the end-call button, a refusal line.
userBubbleStringThe user's chat bubbles.
userBubbleTextStringText in the user's bubbles.
agentBubbleStringThe agent's chat bubbles.
agentBubbleTextStringText in the agent's bubbles.
scrimStringWhat dims the app behind a sheet or dialog, drawn at 40 %.

GraineThemeTokens.Font

Read it to set your own text in the design's typefaces, sizes and weights. data class Font(…), as GraineThemeTokens.font.

FieldTypeMeaning
bodyStringThe body text's family: system is the platform's own UI font; any other name is a family your app may bundle.
headingStringThe titles' family, read the same way.
scaleScaleThe type sizes, in sp (GraineThemeTokens.Font.Scale).
weightsWeightsThe weights (GraineThemeTokens.Font.Weights).
letterSpacingPairLetter spacing for body text and titles, in em (GraineThemeTokens.Font.Pair).
lineHeightPairLine height for body text and titles, as a multiple of the size.

GraineThemeTokens.Font.Scale

Read it to size your own text on the design's scale. data class Scale(val caption: Double, val body: Double, val title: Double, val display: Double), in sp.

FieldTypeMeaning
captionDoubleSmall text: captions, labels and times.
bodyDoubleBody text and chat messages.
titleDoubleTitles, such as a header's.
displayDoubleThe largest text, such as the call screen's name.

GraineThemeTokens.Font.Weights

Read it to weight your own text the way the design does. data class Weights(val body: Int, val strong: Int, val title: Int): CSS weights, 100–900 — 400 is regular, 600 semibold.

FieldTypeMeaning
bodyIntBody text.
strongIntEmphasis: labels and buttons.
titleIntTitles.

GraineThemeTokens.Font.Pair

Read it for a value that differs between body text and titles. data class Pair(val body: Double, val title: Double) — letter spacing in em, or line height as a multiple of the size, as its field says.

FieldTypeMeaning
bodyDoubleThe value for body text.
titleDoubleThe value for titles.

GraineThemeTokens.Shape

Read it for the design's corners, outline width and launcher shape. data class Shape(val radius: Radius, val borderWidth: Double, val launcher: String), as GraineThemeTokens.shape.

FieldTypeMeaning
radiusRadiusThe corner radii (GraineThemeTokens.Shape.Radius).
borderWidthDoubleThe width of every outline, in dp.
launcherStringThe launcher's outline: circle, squircle, rounded-square, square or pill.

GraineThemeTokens.Shape.Radius

Read it to round your own corners the way the design does. data class Radius(val sm: Double, val md: Double, val lg: Double, val xl: Double), in dp.

FieldTypeMeaning
smDoubleSmall corners: chips, a square launcher.
mdDoubleButtons and text fields.
lgDoubleCards, and a rounded-square launcher.
xlDoubleThe roundest corners: pill-shaped chips, sheets, the voice bar.

GraineThemeTokens.Spacing

Read it to space your own UI on the design's rhythm. data class Spacing(val unit: Double, val cardPadding: Double, val gap: Double), in dp.

FieldTypeMeaning
unitDoubleThe base step: other spacing is a multiple of it.
cardPaddingDoubleThe padding inside a card.
gapDoubleThe gap between items in a list or a row.

GraineThemeTokens.Elevation

Read it to give your own cards and floating views the design's depth. data class Elevation(val card: Shadow?, val floating: Shadow?, val hardOffset: Offset?), as GraineThemeTokens.elevation.

FieldTypeMeaning
cardShadow?The shadow under cards (GraineThemeTokens.Shadow); null for none.
floatingShadow?The shadow under what floats — the voice bar, the launcher, sheets; null for none.
hardOffsetOffset?A solid offset drawn instead of soft shadows (GraineThemeTokens.Offset); null unless the design draws depth that way.

GraineThemeTokens.Shadow

Read it to draw one of the design's soft shadows. data class Shadow(val y: Double, val blur: Double, val spread: Double, val opacity: Double, val color: String), used by GraineThemeTokens.Elevation.

FieldTypeMeaning
yDoubleHow far down the shadow falls, in dp.
blurDoubleHow soft it is: the blur radius, in dp.
spreadDoubleHow far it grows past the shape, in dp.
opacityDoubleHow dark it is, 0–1, applied to color.
colorStringThe shadow's colour, #RRGGBB.

GraineThemeTokens.Offset

Read it to draw the solid, offset shadow some designs use instead of a soft one. data class Offset(val x: Double, val y: Double): the shape repeated behind itself, drawn in colors.text, as GraineThemeTokens.elevation.hardOffset.

FieldTypeMeaning
xDoubleHow far right it sits, in dp (negative: left).
yDoubleHow far down it sits, in dp (negative: up).

GraineThemeTokens.Motion

Read it to move your own UI at the design's pace — or not at all. data class Motion(val durationMs: Int, val easing: String, val bounce: Double), as GraineThemeTokens.motion. The phone's own reduce-motion setting wins over any number here.

FieldTypeMeaning
durationMsIntHow long a transition runs, in milliseconds; 0 means no transition at all.
easingStringThe curve: standard, emphasized, spring, linear or none (no movement).
bounceDoubleHow much a spring overshoots, 0–1.

GraineThemeTokens.Icons

Read it to match the design's icon style and the launcher's glyph. data class Icons(val style: String, val stroke: Double, val launcher: Glyph?), as GraineThemeTokens.icons.

FieldTypeMeaning
styleStringThe style of the SDK's icons: outline, filled, duotone or sharp.
strokeDoubleThe line width of icons and stroked glyphs, in dp.
launcherGlyph?The launcher's glyph (GraineThemeTokens.Glyph); null when the launcher shows the brand's logo.

GraineThemeTokens.Glyph

Read it to draw the brand's launcher glyph yourself, from its path. data class Glyph(val id: String, val path: String, val stroke: Boolean), as GraineThemeTokens.icons.launcher.

FieldTypeMeaning
idStringThe glyph's name, such as waveform.
pathStringThe drawing: SVG path data on a 24 × 24 grid.
strokeBooleantrue: draw it as a line icons.stroke wide; false: fill it.

GraineThemeTokens.Glass

Read it to draw translucent surfaces the way a glass design does. data class Glass(val blur: Double, val surfaceOpacity: Double), as GraineThemeTokens.glass — null for an opaque design.

FieldTypeMeaning
blurDoubleHow much what is behind a translucent surface is blurred, in dp, where the platform can.
surfaceOpacityDoubleThe opacity colors.surface is drawn at over that blur, 0–1.

GraineThemeTokens.Chat

Read it to draw a chat window of your own the way the agent's design does. data class Chat(…), as GraineThemeTokens.chat: the Garage's chat window, resolved. Colours are #RRGGBB; an empty word means the SDK's own default.

FieldTypeMeaning
titleStringThe header's title; empty: the agent's heading.
subtitleStringThe header's second line while online; empty: “Online”.
avatarStringThe header's mark: logo, glyph or none.
placeholderStringThe message field's placeholder, such as “Message…”.
showTimestampsBooleanWhether each message shows its time.
suggestionsList<String>Up to four replies offered before the user has said anything; a tap sends one.
backgroundStringBehind the messages.
patternStringnone, or dots over the messages in patternColor.
patternColorStringThe dots' colour.
headerBackgroundStringThe header's fill.
headerTextStringThe header's title colour.
headerSubtextStringThe header's second line's colour.
composerBackgroundStringThe message field's fill.
composerTextStringThe message field's text colour.
composerPlaceholderStringThe placeholder's colour.
composerBorderStringThe message field's outline.
sizeGraineChatSize?How big the window opens — the dashboard's Window size (GraineChatSize); null for a word this build does not know: draw it compact.
showUpcomingCallsBooleanWhether the Upcoming calls card — and Book a call — shows at the top of the chat (Booked calls in the dashboard).
upcomingTitleStringThat card's title; empty: “Upcoming call” or “Upcoming calls”.
showHistoryBooleanWhether the conversation kept from before is shown (Earlier messages); off, the agent still has it.
newConversationBooleanWhether the header offers New conversation; off, chat.newConversation() still works.

GraineThemeTokens.Call

Read it to draw a call screen of your own the way the agent's design does. data class Call(…), as GraineThemeTokens.call: the Garage's call screen, resolved. Colours are #RRGGBB.

FieldTypeMeaning
backgroundStringThe screen's fill — the gradient's top when backgroundTo is set.
backgroundToString?The gradient's bottom; null for a solid background.
textStringThe name and the agent's caption.
textMutedStringThe status line, button labels and the user's caption.
avatarStringWhat the avatar shows: logo, glyph or initials.
avatarShapeStringcircle, squircle or rounded-square.
avatarFillStringBehind a glyph or initial.
avatarTextStringThe glyph or initial.
buttonFillStringA control's fill (mute, loudspeaker).
buttonIconStringA control's icon.
buttonActiveFillStringA control's fill while it is on (muted, the loudspeaker).
buttonActiveIconStringA control's icon while it is on.
endFillStringThe end-call button's fill.
endIconStringThe end-call button's icon.
endShapeStringThe end-call button: round or pill.
acceptFillStringThe Answer button's fill.
acceptIconStringThe Answer button's icon.
declineFillStringThe Decline button's fill.
declineIconStringThe Decline button's icon.
callbackFillStringThe Call back button's fill, on a missed call.
callbackIconStringThe Call back button's icon.
captionsBooleanWhether the screen shows the line being said.
timerBooleanWhether it shows how long the call has lasted.
controlsList<String>The in-call controls, in order, from mute, speaker, chat and end.
ringtoneStringThe ring: system (the phone's), graine or soft.
statusTextStatusTextThe screen's words (GraineThemeTokens.Call.StatusText).

GraineThemeTokens.Call.StatusText

Read it for the brand's words on the call screen. data class StatusText(val ringing: String, val connecting: String, val ended: String, val scheduled: String?, val missed: String?).

FieldTypeMeaning
ringingStringWhile an agent's call rings, such as “Incoming call”.
connectingStringWhile the call connects, such as “Connecting…”.
endedStringOnce a call is over, such as “Call ended”.
scheduledString?While a call Graine rang at a booked time rings; null where the design leaves the SDK's own, “Scheduled call”.
missedString?For a ring nobody answered; null where the design leaves the SDK's own, “Missed call”.

Going live

  • Debug logging. GraineConfig(debug = true) in debug builds logs the SDK's traffic with Graine — keys only, never values — to logcat (tag Graine) and the iOS console. Leave it off in release builds.
  • Android, Google Play. On Android 14 and up, Play asks apps that declare USE_FULL_SCREEN_INTENT or the microphone foreground service to justify them; the call screen needs both. An app that never uses calls removes them from its manifest (see the Android reference).
  • iOS. Calls need the Voice over IP and Audio background modes and push notifications; Graine rings only once your VoIP credentials are uploaded in Workspace settings → In-app calling.
  • The version. GraineKmpVersion.NAME says which build of the library the app runs; include it when you ask for support.

GraineKmpVersion

Read it to know which version of the library your app runs — include it when you ask for support.

object GraineKmpVersion

What to expect — The same on Android and iOS.

PropertyTypeMeaning
const val NAME: StringStringThe library's version: "0.10.3" in this release.

Events and listeners

The states are flows that always hold the latest value — Graine.session, Graine.theme, Graine.chat.turns, Graine.chat.status, Graine.chat.upcomingCalls, Graine.voice.state, Graine.calls.state — and the moments are shared flows that are not replayed: Graine.events (each agent's own events), Graine.calls.outcomes and Graine.calls.incoming. Collect the shared flows from app start for what you log or count.

val unread = MutableStateFlow(0)          // the badge on your chat button
val chatOpen = MutableStateFlow(false)    // true while your chat screen shows
scope.launch {
    Graine.events.collect { event ->
        when (event) {
            is GraineEvent.Error -> println("Graine: ${event.message}")   // for your team's logs, never the user
            is GraineEvent.ChatMessage -> if (event.turn.proactive && !chatOpen.value) unread.value += 1
            else -> Unit
        }
    }
}

GraineEvent

Collect it from Graine.events to hear what the agent reports that is not a state.

sealed class GraineEvent

What to expect — Not replayed. A when over it needs an else, so an event added later does not break your build.

CaseMeaning
SessionReady(session: GraineSession)The agent's settings arrived (also in Graine.session) — offer what depends on them.
Caption(role: GraineRole, text: String, live: Boolean)A line of the voice bar's call: the agent's as it is spoken, the user's as it is heard; live while it is still arriving.
VoiceEndedThe voice bar's call is over, however it ended.
Component(component: GraineComponent)The agent drew a component, in the chat or on a call — for an app that draws components itself.
ComponentPending(hint: Map<String, Any?>)A component is on its way and its content is loading — draw a loader.
ChatMessage(turn: GraineChatTurn)The agent finished a message in the chat; turn.proactive means it spoke up unasked — badge your chat if it is closed.
ActionRequested(name: String)The agent asked for one of your actions.
ActionCompleted(name: String, result: GraineActionResult)What one of your actions answered the agent — your handler's result, or the SDK's when it was late or unknown.
Friction(name: String, screen: String?, detail: String)The user seems stuck (stalled_on_step): during a live conversation they have stayed on one screen for a while with a field still to fill. The agent has already been told; badge a control or count it.
MicrophoneDenied(reason: String)The microphone was refused, so the agent cannot hear the user — explain how to allow it.
Error(message: String)Something is misconfigured (a key, an allowed domain). Log it for your team; it is never the user's problem and is not shown to them.

Errors

A suspending function that asks Graine on the user's behalf throws GraineException carrying a GraineErrorCode. Branch on the code; show the message, which is written for the user and never carries the identifier, traits or a push token. Calling anything before Graine.configure throws IllegalStateException instead — a mistake in the app, not a refusal.

GraineException

Catch it around a request to Graine to decide what to do about a refusal.

class GraineException(val code: GraineErrorCode, message: String, val retryAfterSeconds: Int? = null) : Exception(message)

What to expect — code is the stable part — branch on it, never on the message. A refusal made on the device before anything was sent carries the same codes.

PropertyTypeMeaning
val code: GraineErrorCodeGraineErrorCodeBranch on it: the reason in one stable word.
messageStringShow it: Graine's sentence when it sent one, written for the user.
val retryAfterSeconds: Int?Int?Use it to wait before trying again: how long Graine asked (Retry-After), with RateLimited; null when it did not say.

GraineErrorCode

Read it to branch on why a request was refused.

sealed class GraineErrorCode(val wire: String)

What to expect — A code newer than this SDK arrives as Unknown(raw), with Graine's word in it.

PropertyTypeMeaning
wireStringGraine's word for the code — too_soon, network, … — for logs.
CaseMeaning
InvalidKeyThe publishable key was refused. Check it against Embed & Widgets.
OriginRefusedThe agent does not allow apps yet: add app:// to its Allowed domains.
RateLimitedToo many requests; retryAfterSeconds says when to try again, when Graine said.
InvalidRequestA field was missing or malformed — a blank call ID, say.
IdentityRequiredNobody is signed in: identify() the user with the trait the agent's Who is the user? setting names.
DeviceLimitThe user has already reached the agent's limit of devices.
SchedulingOffBooking is switched off for this agent (Calls & scheduling).
TooSoonSooner than the agent's minimum notice.
TooFarFurther ahead than the agent allows.
OutsideWindowThe time falls outside the agent's calling hours, in the user's time zone.
TooManyPendingThe user has already booked the most calls the agent allows.
NotFoundNo such booking for this user or install.
UnavailableGraine could not answer just now. Worth trying again later.
NetworkNothing came back: the phone is offline, or Graine could not be reached. Worth trying again.
Unknown(raw)A code newer than this SDK, or none at all ("unknown"), carrying the word.

Its functions: from — each with its parameters, what it returns, its errors and an example.

CodewireHTTPMeansRaised byFix
InvalidKeyinvalid_key401The publishable key is not one, or belongs to no agentEvery request to GraineCopy the key again from Agent → Embed & Widgets → Install
OriginRefusedorigin_refused403app:// is missing from the agent's Allowed domainsEvery requestAdd app://
RateLimitedrate_limited429Too many requests from this install, key or addressEvery requestWait retryAfterSeconds, then retry
InvalidRequestinvalid_request400A field is missing or malformedscheduleCall, cancelScheduledCallFix the request
IdentityRequiredidentity_required400Nobody is identified, or the identifier is a placeholdermemories, forgetMemory, bookings that need a useridentify() with the identifier first
DeviceLimitdevice_limit403This install is not one of the user's allowed devicesmemories, forgetMemory, newConversation, scheduleCallSign out on another device, raise Devices per user, or Reset devices for the user
SchedulingOffscheduling_off403 (on the device first)Users may not book from the appscheduleCallTurn it on, or hide booking when bookingAvailable is false
TooSoontoo_soon400 (on the device first)Earlier than earliest(now)scheduleCallOffer times from earliest(now)
TooFartoo_far400 (on the device first)Later than latest(now)scheduleCallOffer times up to latest(now)
OutsideWindowoutside_window400The time falls outside the agent's calling hours, in the user's time zonescheduleCallShow the message (it names a time that works); offer another time
TooManyPendingtoo_many_pending409The user already has the most calls waitingscheduleCallCancel one first
NotFoundnot_found404No such booking for this user or installcancelScheduledCallRead scheduledCalls() again
Unavailableunavailable503 (504 on a timeout)Graine could not answer just now — including a booking refused because the agent is switched off or out of creditEvery requestRetry with backoff; check the agent is on and the workspace has credit
Networknetwork—No answer came back: offline, or the connection droppedEvery requestRetry when online
Unknown(raw)the wordotherA code newer than the SDKEvery requestLog the code and message; show the message

When the bar or chat cannot start, the reason arrives as GraineEvent.Error, not as an exception: key_missing, key_malformed (publishable keys start with pk_live_), key_unknown (copy the key again), no_origin (add app://), no_domains_configured (the list is empty), origin_not_allowed, and not_configured (high-quality audio is not enabled for the workspace; calls use the standard path). A refused microphone arrives as GraineEvent.MicrophoneDenied.

Rate limits and quotas

LimitValueWhere it applies
Bookings from one install10 an hourscheduleCall, the booking sheet, and the agent's bookings in that install's conversations
Bookings in a workspace1,000 an hourAll in-app bookings
Calls waiting per user3 by default (1–10)At most this many upcoming calls per user
Devices per user2 by default (1–5)Devices per user; idle devices free their place after 30 days
Rings per user per day3 by default (1–10)Ring each user at most, in the user's own day
Quiet period after a decline6 hoursNo ring for that user
Notice and horizon5 minutes (2–1440) · 14 days (1–30)With at least this much notice · Booked at most this far ahead
Ring time · re-ring45 s (20–60) · once after 10 min (0–120)Ring for · After a missed call, ring once more after
Lateness10 minutesA booked call more than 10 minutes late is not rung
Booking reason · variables120 characters · 12 variables, about 1 KBBookings and calls
Memories listedAt most 100, newest firstmemories()
Conversation kept50 messages, 7 days after the lastConversations that continue
Action handlers6 secondsAfter that, the agent is told the action was not done

Lower-level building blocks

Reading Graine's own words into the SDK's types — for a server or a bridge that passes them around. An integration does not need them.

GraineCallStatus.from

Call it to turn a status word from Graine (or your server) into a GraineCallStatus.

fun from(wire: String?): GraineCallStatus

Case and surrounding spaces do not matter. No word at all reads as Scheduled, as the native SDKs read it.

ParameterTypeDefaultMeaning
wireString?requiredGraine's word, such as "no_device".

Returns — GraineCallStatus — a known status, or Unknown(raw). Example value: GraineCallStatus.NoDevice.

Errors — none.

val status = GraineCallStatus.from("completed")

GraineCallSource.from

Call it to turn a source word from Graine into a GraineCallSource.

fun from(wire: String?): GraineCallSource?

sdk and app are App; case does not matter.

ParameterTypeDefaultMeaning
wireString?requiredGraine's word, such as "sdk".

Returns — GraineCallSource? — null for no word at all. Example value: GraineCallSource.App.

Errors — none.

val source = GraineCallSource.from("dashboard")

GraineErrorCode.from

Call it to turn an error word from Graine (or a native SDK) into a GraineErrorCode.

fun from(wire: String?): GraineErrorCode

Case does not matter; a blank word is Unknown("unknown").

ParameterTypeDefaultMeaning
wireString?requiredThe word, such as "too_soon".

Returns — GraineErrorCode — a known code, or Unknown(raw). Example value: GraineErrorCode.TooSoon.

Errors — none.

val code = GraineErrorCode.from("rate_limited")

GraineCallTransport.ofCall

Call it to turn a platform SDK's word for what carried a call into a GraineCallTransport — the way GraineCallInfo.transport is read.

fun ofCall(wire: String?): GraineCallTransport?

Case does not matter. Only what a call can run on is read: auto is something an app chooses, never what carried a call.

ParameterTypeDefaultMeaning
wireString?requiredThe word, such as "webrtc".

Returns — GraineCallTransport? — WEBRTC or WEBSOCKET; null for any other word, or none. Example value: GraineCallTransport.WEBRTC.

Errors — none.

val transport = GraineCallTransport.ofCall("websocket")

What's new in 0.9.0

  • WebRTC calls. Add each platform's WebRTC module to the app and native calls — the ones the person places and the ones Graine rings — carry their audio over WebRTC: steadier on a weak network, with the same ring, call screen and outcomes. GraineCallsConfig(callTransport = …) chooses (GraineCallTransport); GraineCallInfo.transport and transportFallbackReason say which one carried a call (WebRTC calls).
  • The brand's design tokens in shared code: Graine.theme (and each agent's theme), a GraineThemeTokens, following the phone between light and dark.
  • Graine.currentVariables() (and each agent's): every prompt variable the next conversation carries, merged in precedence order.
  • The chat: clear() empties the screen only; upcomingCalls, refreshUpcomingCalls() and cancelUpcomingCall(id) give a chat of your own the Upcoming calls card; answerWidget(id, data, summary) answers a component you draw yourself.
  • What was said on a voice-bar call, and the captions switch: Graine.voice.state carries captions (the last 12 lines) and captionsOn; setCaptionsOn(on) and clearCaptions().
  • On Android, chat.status is RECONNECTING whenever the Android SDK is making the chat again — after a first try that failed too, as on iOS.
  • scheduleCall's own refusals say the booking sheet's words: “Booking a call isn't available right now”, “Pick a time at least 5 minutes from now”, “Pick a time within 14 days”.
  • On iOS, link GraineInApp of this version (and GraineInAppRTC at the same version): with an older one the new chat, voice-bar, design and variables members stay empty or do nothing.
  • The full list of changes is in the Changelog.

What's new in 0.8.0

  • Booking a call, drawn by the SDK. Graine.presentBooking(reason) (and GraineAgent.presentBooking) shows the native SDK's booking sheet — the day, the time (each platform's own time picker), the phone's time zone, What's it about? (optional) and Book call — in the agent's design, and returns the booked call, or null when the user closed it without booking. Refusals stay in the sheet as one line.
  • bookingAvailable, on Graine and each agent: Graine rings this phone, the agent lets users book, and the user is identified. Show your own “Call me” button only then.
  • On iOS the sheet is GraineInApp's, so link GraineInApp 0.8.0: with an older one, bookingAvailable is false and presentBooking throws Unavailable, and the app keeps running.
  • On iOS, VoiceOver now reaches into GraineChat(), GraineCallScreen() and GraineAgentBar().
  • The SDK's chat windows (GraineChat(), GraineViews.chat()) offer Book a call in their Upcoming calls card by the same rule.
  • Calls hold up on weak networks (the native SDKs, 0.8.0): on Android a slow uplink no longer holds the call up, hanging up lets go of the microphone at once, and a connection that has gone silent for 50 seconds ends the call; on iOS a call that cannot connect fails within 20 seconds, and one whose audio never starts says so.
  • The full list of changes is in the Changelog.

Every symbol, A to Z

Every public symbol of the Kotlin Multiplatform SDK — 544 in all — by name, each linked to where this page documents it: a type or function first, then its members.

On this page

Requirements, install, permissions and initialisationWebRTC calls (optional)Install and the first answerGraine.configureGraine.configure (Android, with a context)GraineGraine.session (the agent's settings)GraineConfigGraineSessionGraineInitProviderIdentity, memory and conversations that continueGraine.identifyGraine.setMemoryGraine.memoriesGraine.forgetMemoryGraineAgent.memoriesGraineAgent.forgetMemoryGraineChat.newConversationGraineMemoryChatGraineChatGraineChat.openGraineChat.closeGraineChat.sendGraineChat.retryGraineChat.clearGraineChat.refreshUpcomingCallsGraineChat.cancelUpcomingCallGraineChatTurnGraineDeliveryGraineChatChannelGraineChatStatusGraineChatSizeGraineRoleGraineChat (Compose)GraineViewsGraineViews.chatGraineChatRowsGraineChatRows.buildGraineChatRows.dayLabelGraineChatRows.callLabelGraineChatRows.offersStartersGraineChatRows.offersNewConversationGraineChatRowGraineChatDayThe voice barGraineVoiceGraineVoice.startGraineVoice.endGraineVoice.setMutedGraineVoice.setCaptionsOnGraineVoice.clearCaptionsGraineVoiceInfoGraineCaptionGraineTransportGraineAgentBarGraineViews.voiceBarGraine.onRequestPermissionsResultSeveral agents in one appGraine.agent (by name)GraineAgentScreen context, actions and componentsGraine.setScreenGraine.updateFieldGraine.removeFieldGraine.clearScreenGraine.trackGraine.tapGraine.noteInteractionGraine.setVariablesGraineAgent.setVariablesGraine.currentVariablesGraineAgent.currentVariablesGraine.registerActionGraineAgent.registerActionGraineScreenGraineScreen.JourneyGraineFieldGraineFieldStatusGraineActionResultGraineActionResult.StatusGraineActionResult.okGraineActionResult.refusedGraineActionResult.errorGraineComponentGraineChat.answerWidgetCalls the user placesGraineCallsGraineCalls.configureGraineCallsConfigGraineCallTransportGraineCalls.startCallGraineAgent.startCallGraineCalls.endGraineCalls.setMutedGraineCalls.setSpeakerGraineCallInfoGraineCallStateGraineCallOutcomeGraineCallOutcome.KindGraineCallScreenGraineViews.callScreenCalls Graine ringsGraineCalls.enableVoIPPushesGraineCalls.onNewTokenGraineCalls.handlePushGraineCalls.answerGraineCalls.declineGraineCalls.callBackGraineCalls.dismissMissedCallGraineIncomingCallGraineMissedCallPermissions and the setup checkGraineCalls.callPermissionsGraineCalls.requestCallPermissionsGraineCalls.openCallSettingsGraineCalls.callSetupProblemsGraineCallPermissionGraineCallPermissionStatusGraineCallPermissionsGraineCallSetupProblemBooking with the SDK's sheetGraine.bookingAvailableGraine.presentBookingGraineAgent.bookingAvailableGraineAgent.presentBookingThe scheduled-calls listGraineSchedulingRulesGraineSchedulingRules.earliestGraineSchedulingRules.latestGrainePlatformGraine.scheduleCallGraineAgent.scheduleCallGraine.scheduledCallsGraineAgent.scheduledCallsGraine.cancelScheduledCallGraineAgent.cancelScheduledCallGraineScheduledCallGraineCallStatusGraineCallSourceDesignGraine.themeGraineThemeTokensGraineThemeTokens.ColorsGraineThemeTokens.FontGraineThemeTokens.Font.ScaleGraineThemeTokens.Font.WeightsGraineThemeTokens.Font.PairGraineThemeTokens.ShapeGraineThemeTokens.Shape.RadiusGraineThemeTokens.SpacingGraineThemeTokens.ElevationGraineThemeTokens.ShadowGraineThemeTokens.OffsetGraineThemeTokens.MotionGraineThemeTokens.IconsGraineThemeTokens.GlyphGraineThemeTokens.GlassGraineThemeTokens.ChatGraineThemeTokens.CallGraineThemeTokens.Call.StatusTextGoing liveGraineKmpVersionEvents and listenersGraineEventErrorsGraineExceptionGraineErrorCodeRate limits and quotasLower-level building blocksGraineCallStatus.fromGraineCallSource.fromGraineErrorCode.fromGraineCallTransport.ofCallWhat's new in 0.9.0What's new in 0.8.0Every symbol, A to Z