Types
Every structure the in-app SDKs hand your app, on every platform — chat turns, the chat's state and size, memories, session settings, booked calls and their statuses, scheduling rules, call outcomes, rings and missed calls, the call's state, errors, agents, screens, actions and components — with each field, its type and default, and what to expect from it.
The same structures, with the same fields, on every platform. Each section gives the fields, how each platform declares the type, a JSON example of a real value, and what to expect: when it arrives or changes, what can be empty, and what a newer value looks like to an older SDK.
Times are moments, never local wall-clock times. In the JSON they are
ISO-8601 strings in UTC; in code they are the platform's own type — Date
(TypeScript, Swift), java.util.Date (Android), DateTime (Dart) or Instant
(Kotlin Multiplatform). Show them in the device's time zone. Flutter hands times
that came from Graine (bookings, memories, earlier chat messages) as UTC
DateTimes: .toLocal() them for display. The few fields that are not a
Date — React Native's chat and call times, Android's call timer — say so below.
Enums are spelled in each language's own style: a wire word such as
no_device is "noDevice" in TypeScript, .noDevice in Swift, NO_DEVICE on
Android, noDevice in Dart and NoDevice in Kotlin Multiplatform.
A value newer than your SDK never crashes it
A status, source or error code Graine adds later reaches an older SDK as
unknown, carrying the word it was sent: .unknown(raw) in Swift,
Unknown(raw) on Android and in Kotlin Multiplatform, the raw string itself in
TypeScript and on the website, and unknown in Dart with the word beside it
(statusName, sourceName, codeName). Always keep a default branch.
Chat
GraineChatTurn
One message in a chat's transcript, oldest first: the customer's, or one reply of the agent's.
| Field | Type | |
|---|---|---|
id | String | Stable while the transcript lives: t1, t2, … for this device's turns; Graine's own id for an earlier message put back when the conversation continued |
role | The customer, or the agent | Spelled per platform — below |
text | String | What was said. An agent's turn may be only a component, with empty text |
at | A time | When it was said — for an agent's turn, its first words |
delivery | sending, sent or failed | The customer's turns: waiting for a connection, sent, or not sent because the chat could not reconnect. The agent's turns are always sent (null in Flutter and Kotlin Multiplatform) |
proactive | Bool, default false | The agent's turns: it spoke without being asked — its welcome, or something it noticed |
live | Bool, default false | The agent's words are still arriving: draw them as provisional |
component | A component, or null | What the agent drew with this turn — Android, Flutter, and Kotlin Multiplatform on Android. iOS and React Native hand components to their component views instead (GraineWidgetView, useGraineWidget) |
restored | Bool, default false | An earlier message, put back when the conversation continued. React Native: restored?: true |
channel | chat or voice, default chat | Where it was said. Only an earlier message can be voice: something said on a call. React Native: channel?: "voice", absent for the chat |
React Native has no failed: a message waits, pending, and goes the moment
the chat is back — the chat keeps trying for as long as the customer is in it.
When the chat itself gives up — its first connect failed, or Graine ended or
refused the conversation — useGraineAgent().failed says so, and retry()
connects it again.
What to expect
- Earlier messages arrive once, first. For an identified customer with memory
on, a chat that opens with this agent within 7 days of their last message
starts with up to 50 earlier messages, oldest first, before the agent says
anything — and there is no second welcome. They are delivered, never live,
never proactive, with the time each was said,
restoredset, and Graine's ids. Anything of the customer's still waiting to go follows them. A reconnect brings them again; they replace the earlier copies, so nothing shows twice. For an anonymous customer, a customer who turned memory off, or an agent without memory, nothing is restored. - A voice call is one row. A run of
voiceturns on one day is something said on a call: the SDK's chat window draws it as one Voice call · 6:40 pm row, never as bubbles, and so should yours. A date line (Today, Yesterday, or the date) goes wherever the day changes. Every SDK lays this out for you: GraineChatRows. liveturns change. An agent's turn grows as its words arrive and settles (livefalse) when the turn ends, when the customer speaks, or when the connection goes. Redraw on every change; the next reply is a new turn.failedwaits for a tap. The SDK's chat window marks a failed message as not sent; a tap — orretry()(iOS, Kotlin Multiplatform; Androidconnect(), Flutteropen()), the next message, or opening the chat again — sends it.- The transcript clears when
forgetMemory()succeeds, onnewConversation(), onidentify(null), and when a different customer is identified on the device. A device keeps at most the last 200 turns on screen.
An earlier message, put back when the conversation continued, and part of a call:
GraineChatRows
Every SDK lays a conversation out the way its chat window draws it, for an app
that draws its own chat (the SDK's own window already does): React Native
chatTurnRows(messages) (chatRows(items, read) for items of your own), iOS
GraineChatRows.build(turns) (GraineChatRow: .day(GraineChatDayLine),
.voice(GraineChatCallLine), .turn), Android ChatRows.build(messages)
(ChatRows.Row: Day, Voice, Turn), Flutter ChatRows.build(turns, now:)
(ChatDayRow, ChatVoiceRow, ChatTurnRow), and Kotlin Multiplatform — from
Graine.chat.turns (the SDK's GraineChat() already does): a bubble per chat
turn, one Voice call row for a run of voice turns on one day, and a day
separator before the first row when it is not from today and wherever the
device's day changes.
What to expect: days are the device's (or zone's); a date from another
year carries the year (Wed 31 Dec 2025). A conversation all from today has no
separator and lays out exactly as before. Rebuild the rows whenever turns
changes, and key your list on key.
The chat's state
Whether the chat is connected, and what to say while it is not.
| Platform | Where | Values |
|---|---|---|
| React Native | useGraineAgent() | connected, connecting, reconnecting, failed (booleans), typing; client.chatFailed; client.conversation: "chat", "call" or null — which conversation this agent holds now |
| iOS | GraineChat.status | GraineChatStatus, below; online and status.isConnecting as shortcuts; typing |
| Android | GraineChat | connected, connecting, reconnecting, failed (booleans), typing; also onChatState(connected, connecting) on GraineListener |
| Flutter | GraineChatSession | connected, connecting, reconnecting, failed (booleans), error (a sentence, or null), isOpen, agentTyping |
| Kotlin Multiplatform | Graine.chat.status | GraineChatStatus (a StateFlow), below; typing |
| Website | — | The widget draws its own state; the page receives none |
GraineChatStatus (iOS, Kotlin Multiplatform):
| Value | Means |
|---|---|
idle | Not in the chat: never opened, closed, or the app is in the background |
connecting | Opening |
online | Connected: a message goes now |
reconnecting | The connection dropped and is being made again — iOS: reconnecting(attempt:), the attempt of six |
offline | It could not reconnect, or the agent ended the conversation. A message, or retry(), connects again |
What to expect: a dropped chat reconnects by itself while the customer is in
it. iOS, Android, Flutter and Kotlin Multiplatform try six times — 1, 2, 4, 8, 15
and 15 seconds apart — then stop, offline (failed), until the customer sends
a message or your app calls retry() or opens the chat again. React Native waits
1, 2, 4, 8, 15, then 30 seconds between tries, for as long as the customer is in
the chat; it stops — failed — only when its first connect failed or Graine
ended or refused the conversation. None reconnects a chat closed on purpose, one Graine refused, or one a
call replaced. Say Reconnecting…, not Offline, while it tries. The typing
indicator runs from the customer's message to the agent's first words, a
component, the chat closing, or a while with no answer (12 seconds; 30 on iOS).
GraineChatSize
How big the chat window opens: compact (the window above the bar), large (a sheet over most of a phone, a panel on a tablet or a computer) or full (the whole screen). What each looks like: Chat → Window size.
| Platform | The type | The app's size | One open's size | The size in force |
|---|---|---|---|---|
| React Native | ChatSize: "compact", "large", "full" | chatSize on GraineProvider | client.openChat({ size }) | useGraineAgent().chatSize |
| iOS | enum GraineChatSize: String { case compact, large, full } | GraineConfig(chatSize:) | chat.open(size:) | chat.openedSize (an open's own, until close()) |
| Android | enum class GraineChatSize(val wire: String) { COMPACT, LARGE, FULL } | GraineConfig(chatSize = …) | chat.open(size) | chat.openedSize |
| Flutter | enum GraineChatSize { compact, large, full } | GraineConfig(chatSize: …) | GraineChat.open(context, size: …) | — |
| Kotlin Multiplatform | enum class GraineChatSize(val wire: String) { COMPACT, LARGE, FULL } | GraineConfig(chatSize = …) | Graine.chat.open(size) | — |
| Website | "compact", "large", "full" | GraineAgent.load({ chatSize }) | GraineAgent.open({ mode, size }) | — |
What to expect
- One open's size wins, then the app's, then the design's (the Garage's Chat window → Window size), else compact. Leave the app's size unset to follow the dashboard. An open's size lasts until the chat closes; moving to the call and back keeps it.
- A word an SDK does not know — from a newer dashboard, or a typo on a
website — is passed over, never an error: the next choice applies. Android's
GraineChatSize.from(wire)returns null for one. - Flutter with no bar on screen:
GraineChat.openpresents the chat on the navigator —largeas a sheet (a panel on a tablet),fullas a screen of its own, andcompact, which belongs to the bar, aslarge. With a bar on screen, the bar moves to the chat at that size. GraineChatView,GraineChatViewController,GraineChatActivity,GraineChatand React Native'sGraineChatScreenfill whatever space your app gives them: the size is for the window the bar or the widget opens.
newConversation
Starts the chat over with this agent: a fresh thread.
| Platform | Declaration | Returns |
|---|---|---|
| React Native | client.newConversation(), useGraineAgent().newConversation() | Promise<void> |
| iOS | GraineChat.newConversation() async throws | nothing; throws GraineServiceError |
| Android | GraineChat.newConversation(done: GraineCallback<Unit>? = null) | done(Unit, null), or done(null, error) — on the main thread |
| Flutter | GraineChatSession.newConversation() | Future<void>; throws GraineException |
| Kotlin Multiplatform | suspend fun GraineChat.newConversation() | Unit; throws GraineException |
| Website | GraineAgent.newConversation() | Promise<void>; rejects with an error carrying code |
What to expect
- Success: the chat's connection ends, Graine lets go of the conversation it kept for this agent and the identified customer, the transcript clears (with anything still waiting to go), and — when the customer was in the chat — a fresh chat opens, which greets as a first chat does.
- Memory stays. What the agent remembers about the customer is untouched, and so are other agents' conversations.
- Anonymous: nothing was kept, so nothing is asked of Graine — the transcript clears, and it cannot fail.
- Failure: it fails as
forgetMemory()does —device_limit,unavailable,network,rate_limited— and changes nothing: the chat is as it was, and connected again if it was open.
Identity and memory
What identify sends and keeps
identify(traits, memory) names the signed-in customer — Identity and memory → identify.
| The identifier | The value of the trait the agent's Who is the user? setting names (user_id unless it names another). Sent as it is, only on the requests that start a conversation or ask on the customer's behalf — never as a prompt variable. A placeholder (guest, unknown, 0…), an empty value, a boolean or a decimal is no identifier |
| Every other trait | Masked on the device, then a prompt variable |
| What Graine keeps | The identifier, so your team sees who each conversation was with in Call History. Memory is stored under a one-way key derived from it, never the value |
memory | false: this customer's conversations are neither remembered nor recalled. Kept for them on this install; null keeps their last choice (true until they make one) |
| Sign-out | identify(null): the device stops being theirs (it is no longer rung for them and no longer counts toward their devices), the chat on screen clears, and later conversations are anonymous |
| When it applies | From the next conversation; one already running keeps the identity it started with |
GraineMemory
One thing the agent remembers about the signed-in customer, already redacted.
| Field | Type | |
|---|---|---|
id | String | The memory's id |
text | String | What is remembered, redacted before it was stored |
createdAt | A time, or null | When it was stored |
updatedAt | A time, or null | When it last changed; null when it never has |
Declared as GraineMemory everywhere — an Identifiable struct in Swift, a data
class on Android and in Kotlin Multiplatform, a class in Dart, an interface in
TypeScript, a plain object on the website.
What to expect: memories() returns at most 100, newest first. It answers
only for an identified customer (identity_required otherwise) on one of their
devices (device_limit otherwise). A memory without an id or text is skipped,
never handed over half-made.
forgetMemory's result
An integer: how many memories were deleted — Promise<number> (React Native,
website), Int (iOS async throws, Kotlin Multiplatform suspend),
Future<int> (Flutter), and the callback's Int on Android.
What to expect: 0 is a success too — there was nothing to forget. Only when
it succeeds, the conversation kept to continue is forgotten with the memories and
the chat on screen clears. A failure — unavailable (503), device_limit,
network, identity_required — deletes nothing and leaves the chat on screen as
it is.
Session settings
What the SDK reads from the agent's settings once per launch.
| Value | Type | |
|---|---|---|
| identity field | String, default user_id | The trait that names the customer — user_id unless the agent's settings choose email, phone or a custom trait |
| devices per user | Int, 1–5, default 2 | How many installs can act as one customer |
| memory enabled | Bool, or null | The agent remembers its customers; null from an older server |
| hosted calls | Bool, platforms, ring timeout, call name | Graine rings this workspace's apps, and how |
| call audio | "webrtc", "websocket", or absent | The agent's In-app call audio setting (Calls), as calls.audioTransport. "websocket" (Standard) puts every call on the standard audio path, whatever your app chose; absent — the default — or "webrtc" lets the SDK use WebRTC where your app includes it |
| scheduling rules | GraineSchedulingRules | What customers may book |
| Platform | Where |
|---|---|
| React Native | client.config: identity (field, devicesPerUser), memory (enabled), calls (hosted, platforms, ringTimeoutSeconds, callName, scheduling); client.schedulingRules |
| iOS | GraineInApp.session (GraineSessionConfig): identity (GraineIdentitySettings: field, devicesPerUser), memoryEnabled, calls (GraineHostedCallsSettings: hosted, platforms, ringTimeoutSeconds, callName, usersCanBook, … and ringsThisPlatform), schedulingRules |
| Android | GraineInApp.get().session and onSessionReady(session), a SessionConfig: identity (SessionIdentity: field, devicesPerUser), memoryEnabled, schedulingRules (with ringsAndroid) |
| Flutter | GraineInApp.session (SessionConfig): identity and identitySetting (field, devicesPerUser), memory (enabled), calls (hosted, platforms, ringTimeoutSeconds, callName, scheduling); schedulingRules |
| Kotlin Multiplatform | Graine.session (a StateFlow<GraineSession?>): agentId, identityField, devicesPerUser, memoryEnabled, schedulingRules, declaredActions |
| Website | GraineAgent.schedulingRules |
What to expect: null (or the defaults) until the settings load — a moment
after start-up, then kept for the launch. An older server sends none of the
newer values: the identity field is then user_id, two devices, and nobody may
book. A change in the dashboard reaches an app the next time it starts.
IdentityChannel
React Native's identityChannel(): what the SDK will send with the next
conversation.
| Field | Type | |
|---|---|---|
value | String, or null | The identifier, as it is sent — never a prompt variable. Null when nobody is identified |
installId | String, or null | This install. Null when there is nowhere to keep one |
memory | Bool | The customer's memory choice |
timezone | String, or null | The device's time zone, IANA |
Scheduling
GraineScheduledCall
A call booked for later — returned by scheduleCall and scheduledCalls, and
shown in the chat's upcoming calls.
| Field | Type | |
|---|---|---|
id | String | Graine's id for the booking — what cancelScheduledCall takes |
at | A time, or null | When it rings. Null only if Graine's time for it could not be read |
status | GraineCallStatus | Where it is; scheduled when Graine says nothing |
source | GraineCallSource, or null | Who booked it; null when Graine does not say |
reason | String, or null | What it is about, up to 120 characters |
isUpcoming | Bool | status is scheduled or ringing |
canCancel | Bool | status is scheduled |
What to expect: a booking is for the identified customer — or, with nobody
identified, for this install — and rings their newest app device, in their time
zone. Its reason is kept by Graine for the agent and never travels in the push
that rings it. scheduledCalls() lists those still to come, soonest first; read
the status again to learn what became of one.
GraineCallStatus
Where a booked call is. The dashboard and your webhooks use the wire words; the SDKs use the enum.
| Status | Wire | Means |
|---|---|---|
scheduled | scheduled | Booked, waiting for its time — or held a little while for credit or a switched-off agent |
ringing | ringing | Ringing on the customer's device now |
answered | answered | Picked up; the conversation is on |
completed | completed | The conversation ended |
declined | declined | The customer declined it. Nothing rings them again for at least 6 hours, and a declined call never re-rings |
missed | missed | Nobody answered within the ring time |
failed | failed | It could not ring or connect |
cancelled | cancelled | Called off by the customer, your app or your team, before it rang |
expired | expired | It could not ring in time: more than 10 minutes late, held too long, or no time left inside the calling hours and the booking window |
noDevice | no_device | No app device of the customer's was seen in the last 30 days, so there was nothing to ring |
unknown | anything else | A newer status, carrying its word |
Swift and Kotlin types also give wire (the word) and a constructor from it:
GraineCallStatus(wire:), GraineCallStatus.from(wire). Flutter gives
statusWire on the booking. A booking with no status at all reads as
scheduled.
GraineCallSource
Who booked a call.
| Source | Wire | |
|---|---|---|
agent | agent | The agent, during a conversation |
app | sdk | Your app or website, through scheduleCall |
dashboard | dashboard | Your team, in the dashboard |
api | api | The API |
unknown | anything else | A newer source, carrying its word |
source is null when Graine does not say. Swift, Android and Kotlin
Multiplatform also accept app as the wire word.
GraineSchedulingRules
What the agent allows, read from its settings when the SDK loads them — for a booking screen that offers only times that will be accepted.
| Field | Type | Default | |
|---|---|---|---|
usersCanBook | Bool | false | Users, from your app is on. false when the agent's settings say nothing |
minLeadMinutes | Int | 5 | The soonest a booking may be |
maxDaysAhead | Int | 14 | The furthest ahead, in days |
maxPendingPerUser | Int | 3 | Calls a customer may have waiting |
ringTimeoutSeconds | Int | 45 | How long a ring Graine sends lasts |
callName | String, or null | null | What the customer's phone shows when it rings; null: your app's own name |
hostedRinging | Bool | false | Graine rings this workspace's apps (the push credentials are uploaded) |
platforms | List of ios, android | empty | Where Graine rings; empty means every platform |
earliest(now) | A time | now plus minLeadMinutes | |
latest(now) | A time | now plus maxDaysAhead days |
What to expect: the defaults — and no booking — until the agent's settings
load. scheduleCall checks a time against these rules on the device, before
any request: scheduling_off when usersCanBook is false, too_soon before
earliest(now), too_far after latest(now) (React Native and the website
allow 30 seconds' grace on too_soon, for the moment between computing
earliest and sending). The calling hours and the calls already waiting are
Graine's to judge: outside_window and too_many_pending come back from it.
What the scheduling calls return
| Call | Returns | When it resolves |
|---|---|---|
scheduleCall(at, reason) | A GraineScheduledCall, status scheduled | Once Graine has booked it |
scheduledCalls() | A list of them, soonest first; empty when there are none | One request |
cancelScheduledCall(id) | Its GraineCallStatus afterwards: cancelled | Once Graine has called it off |
Each platform's way: a promise (React Native, website), async throws (iOS), a
GraineCallback on the main thread (Android), a Future (Flutter), a
suspend function (Kotlin Multiplatform). Parameters and errors:
Scheduling.
Calls
GraineCallOutcome
What became of a native call on this device — sent to your app as it happens, and, for a call Graine rang, reported to Graine by the SDK itself.
| Field | Type | |
|---|---|---|
callId | String | Your server's call_id, Graine's id for a call it rang, or the id startCall returned |
outcome | ringing, answered, declined, missed, ended or failed | What happened |
reason | String, or null | With ended: local (the customer hung up), remote (the agent did) or failed (the line broke). With failed: why it never connected — a sentence for your logs. With missed, where a platform says: busy (another call was live) or no_answer |
durationSeconds | Int, or null | With ended: how long it was connected, in whole seconds |
hosted | Bool, default false | Graine rang it, rather than your own server |
busy | Bool, default false | Android: a missed ring refused because another call was live |
agent | String, or null | The name of the agent the call was with, "default" for the default agent (React Native, Flutter, Kotlin Multiplatform); null when the platform does not know |
What to expect
- Every outcome carries the call's id; send it to your server with it. An
answered call is followed by
endedorfailed. A call that connected and dropped endsendedwithfailed— calls do not reconnect. - What each platform reports. Kotlin Multiplatform reports every field above
but
busy(itsreasonsaysbusy),ringingincluded; on Android itsagentis null for a ring refused because another call was live. iOS reports the five outcomes, withendReasonanderror; readGraineCalls.agentandGraineCalls.hostedfor the call. Android has noringing(a ring isonIncomingCall) and noagent(calls.callAgent). React Native and Flutter type every field; their 0.7.0 engines sendcallId,outcome,reason(orerror) andagent, and leavedurationSecondsandhostedempty andringingunsent. - Not replayed. Listen from app start. React Native and Flutter hold the last 20 outcomes until a listener arrives; the native SDKs report to the listeners you have.
GraineIncomingCall
A call ringing on this device.
| Field | Type | |
|---|---|---|
callId | String | The call's id |
callerName | String, or null | What the ring shows: the push's caller name, else your calls configuration's display name |
reason | String, or null | The push's reason — never in a ring Graine sends, whose reason reaches the agent when it connects |
variables | Map of strings | The push's prompt variables (iOS variables; Android GraineCall.variables) |
hosted | Bool | Graine rang it (iOS, Android, Kotlin Multiplatform, React Native — hosted?: boolean, present when true; Flutter's field stays false with the 0.7.0 engines) |
agent | String, or null | The configured agent the ring is for (React Native, Flutter, Kotlin Multiplatform), matched from the push's agent id — the default agent when none matches. iOS gives the push's own agentId; Android, calls.callAgent |
| Platform | Declared as | Heard through |
|---|---|---|
| React Native | interface GraineIncomingCall { callId; reason; callerName; agent?; hosted? } | GraineCalls.onIncomingCall(fn) |
| iOS | struct GraineIncomingCall { callId; reason; callerName; variables; hosted; agentId } | GraineCallsListener.onIncomingCall(_:) |
| Android | data class GraineCall(callId, direction, callerName, reason, variables, hosted) | GraineCallListener.onIncomingCall(call) |
| Flutter | class GraineIncomingCall { callId; reason; callerName; hosted; agent } | GraineCalls.instance.incomingCalls |
| Kotlin Multiplatform | data class GraineIncomingCall(callId, callerName, reason, hosted, agent) | Graine.calls.incoming |
| Website | — | — |
GraineMissedCall
A ring nobody answered, kept so a call screen can say Missed call and offer
Call back — iOS (GraineCalls.missedCall) and Kotlin Multiplatform
(GraineCallInfo.missedCall). Android shows a missed-call notification with Call
back instead; React Native and Flutter leave it to the native engine's call
screen and notification.
| Field | Type | |
|---|---|---|
callId | String | The missed call |
callerName | String (Kotlin Multiplatform: or null) | What the ring showed |
reason | String, or null | Its reason, when the push had one |
hosted | Bool | Graine rang it |
at | A time | When it was missed |
agent | String, or null | iOS: the name of the agent that rang; callBack() calls that agent |
What to expect: kept until the customer calls back, dismissMissedCall() is
called, or another call rings or is placed. On iOS showsCallScreen leads with
it for ten minutes. A ring refused because another call was live is not kept.
The call, to draw
What a call screen of your own reads.
| Field | Type | |
|---|---|---|
state | idle, ringing, connecting, active or ended | Where the call is |
callId | String, or null | Null at idle |
direction | incoming or outgoing, or null | Who rang whom |
reason | String, or null | The push's reason, or the one startCall was given |
callerName | String, or null | What the call shows (iOS: an empty string for none) |
muted | Bool | |
speakerOn | Bool | speaker on Android |
agentSpeaking | Bool | The agent's voice is playing |
caption | GraineCaption, or null | The line being said |
connectedAt | A time, or null | When the conversation opened — for a timer |
error | String, or null | Why the last call could not connect (React Native, Flutter); cleared when the next call starts |
hosted | Bool | Graine rang it (iOS, Kotlin Multiplatform) |
agent | String, or null | The name of the agent the call is with (React Native, iOS, Flutter, Kotlin Multiplatform; Android: callAgent, the agent itself); null at idle |
transport | webrtc or websocket, or null | What carries the call's audio, once decided — GraineCallTransport. Null before |
transportFallbackReason | String, or null | Why a call that tried WebRTC runs on the WebSocket — the reasons. Null otherwise |
| Platform | Declared as |
|---|---|
| React Native | GraineCallSnapshot — useGraineCall(), GraineCalls.getSnapshot(). connectedAt is milliseconds since 1970 |
| iOS | Published properties on GraineCalls (state is GraineCallState, with ended(reason)), plus transcript, missedCall and showsCallScreen |
| Android | Properties on GraineCalls: state, call (a GraineCall), muted, speaker, agentSpeaking, caption (role, text, live), connectedAt (the phone's uptime clock in milliseconds, 0 before — subtract it from SystemClock.elapsedRealtime()), callAgent |
| Flutter | GraineCallSnapshot — GraineCalls.instance.value; its state field is status, a GraineNativeCallStatus |
| Kotlin Multiplatform | GraineCallInfo — Graine.calls.state: incoming (a Bool) instead of direction, plus missedCall and showsCallScreen |
| Website | — |
What to expect: one call at a time. ended is reported once, then idle
(on iOS after a second; elsewhere it can pass at once) — take how a call ended
from its outcome, not from the state. A call being placed has no stages here:
connecting until it connects or fails. The stages a bar's call goes through
are in Connection values.
GraineCallTransport
What carries a native call's audio — in both directions, a call the customer
places and a call Graine rang. Chosen in the calls configuration
(callTransport), read back on the call (transport). WebRTC needs the SDK's
WebRTC module in the app (In-app calling → WebRTC).
| Value | As callTransport | As transport |
|---|---|---|
auto | The default: WebRTC when the app includes the WebRTC module and the agent's In-app call audio is not Standard; else the WebSocket | — |
websocket | Always the WebSocket | The call's audio runs on the WebSocket |
webrtc | WebRTC wherever it can carry the call; the WebSocket when the module is missing or WebRTC cannot set this call up | The call's audio runs on WebRTC |
| Platform | Declared as | Chosen in | Read on |
|---|---|---|---|
| React Native | type GraineCallTransport = "auto" | "websocket" | "webrtc" | GraineCalls.configure({ callTransport }) | GraineCallSnapshot.transport, transportFallbackReason |
| iOS | enum GraineCallTransport: String { case auto, websocket, webrtc } | GraineCallsConfig(callTransport:) | GraineCalls.transport (GraineCallTransport?), transportFallbackReason |
| Android | enum class GraineCallTransport { AUTO, WEBSOCKET, WEBRTC } | GraineCallsConfig(callTransport = …) | GraineCalls.transport ("webrtc" / "websocket"), transportFallbackReason |
| Flutter | enum GraineCallTransport { auto, websocket, webrtc } | GraineCallsOptions(callTransport: …) | GraineCallSnapshot.transport, transportFallbackReason |
| Kotlin Multiplatform | enum class GraineCallTransport { AUTO, WEBSOCKET, WEBRTC } | GraineCallsConfig(callTransport = …) | GraineCallInfo.transport, transportFallbackReason |
| Website | — | — | — |
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 — the customer
sees Connecting… a moment longer — and transport says websocket.
Why a call is on the WebSocket
transportFallbackReason, set only for a call that wanted WebRTC and runs on the
WebSocket. Final once the call is active.
| Value | Means |
|---|---|
mint_failed | The call's WebRTC session could not be set up: refused, rate-limited or unreachable |
register_failed | Graine's media service did not accept the connection, or never answered it |
no_response | The call got no reply at all |
no_answer | Replies came; the answer did not |
ice_timeout | Answered, but the audio path never connected — usually a network that blocks WebRTC |
error | Anything else before the audio flowed |
dashboard | Android, when the call would have used WebRTC: the agent's In-app call audio is Standard |
unavailable | Android, when the app asked for webrtc: this build has no WebRTC module, or the device cannot run it |
GraineCaption
| Field | Type | |
|---|---|---|
role | The agent or the customer | agent / customer (USER / AGENT in Kotlin Multiplatform) |
text | String | The line |
live | Bool | Still being said: it is replaced as its words arrive |
Kept as captions on every platform's voice (Android: GraineCaption, new in
0.9.0); the captions switch (captionsOn) hides the bar's caption, never the
list.
startCall's result
The call's id — a string every outcome carries — or null, when nothing was placed:
| Platform | Null when |
|---|---|
| React Native | A call is already live. It rejects for an agent name configure did not name |
| iOS | A call is already live, or no agent has that name |
| Android | A call is already live, or no agent has that name. Call it on the main thread, from an activity in front |
| Flutter | A call is already live, or the build has no native calls |
| Kotlin Multiplatform | A call is already live, or, on Android, no screen of the app is in front |
| Website | — |
The push payload
What a messaging service hands to handlePush — the SDK's own, or your app's
(Android, React Native, Flutter, Kotlin Multiplatform) — or iOS receives as a VoIP push. Every value is a
string.
| Key | |
|---|---|
type | agent_call — anything else is your own message, and handlePush returns false |
call_id | The call's id; required |
caller_name | What the ring shows |
hosted | "1" when Graine rang it |
agent_id | The agent the call is with, when it names one: the app rings with that configured agent |
reason, variables | Only in a push from your own server: the call's reason, and more prompt variables as a JSON object in a string |
A ring Graine sends carries only type, call_id, caller_name, hosted and
agent_id — nothing about the customer, no reason and no variables; the agent
reads those from Graine when the call connects.
Errors
GraineError
What a method that asks Graine fails with.
| Field | Type | |
|---|---|---|
code | GraineErrorCode | Why — branch on this, never on the message |
message | String | A sentence written for the customer; nothing personal in it |
retryAfterSeconds | Int, or null | With rate_limited: how long Graine asked to wait, when it said |
status | Int | The HTTP status, 0 when nothing answered (React Native, iOS, website; statusCode, or null, in Dart) |
| Platform | Declared as |
|---|---|
| React Native, website | class GraineError extends Error { code; status; retryAfterSeconds } |
| iOS | struct GraineServiceError: LocalizedError { code: GraineErrorCode; message; retryAfterSeconds; status } |
| Android | data class GraineError(code: GraineErrorCode, message: String, retryAfterSeconds: Int? = null), in the callback — never thrown |
| Flutter | class GraineException { message; code: GraineErrorCode; codeName; statusCode; retryAfterSeconds } |
| Kotlin Multiplatform | class GraineException(code: GraineErrorCode, message, retryAfterSeconds) |
What to expect: the same codes on every platform — invalid_key,
origin_refused, rate_limited, invalid_request, identity_required,
device_limit, scheduling_off, too_soon, too_far, outside_window,
too_many_pending, not_found, unavailable, network — and a newer one as
unknown, carrying its word. Retry network and unavailable with backoff,
and rate_limited after retryAfterSeconds; fix the setup for invalid_key and
origin_refused; show the message for the scheduling codes. When each happens,
which methods raise it, and the fix: Errors.
Agent instances
An app can run several agents, each under a name with its own publishable key — Use more than one agent in your app, and the signatures on the SDK reference.
| Platform | Configure | Look up | Every agent | The default |
|---|---|---|---|---|
| React Native | One GraineProvider (or client) per agent; the calls package names them: GraineCalls.configure({ agents }) | — | GraineCalls.agents | The root provider; DEFAULT_AGENT ("default") in the calls package |
| iOS | GraineInApp.configure(_:name:) | GraineInApp.named(_:), nil when not configured | GraineInApp.all | GraineInApp.shared, GraineInApp.current |
| Android | GraineInApp.init(application, config, name) | GraineInApp.find(name), null when not configured; get(name) throws | GraineInApp.all | GraineInApp.get(); GraineInApp.DEFAULT |
| Flutter | GraineInApp.init(config, name:) | GraineInApp.named(name), null when not configured | GraineInApp.all | GraineInApp.instance, maybeInstance; GraineInApp.defaultName |
| Kotlin Multiplatform | Graine.configure(config, name) returns a GraineAgent | Graine.agent(name), null when not configured | Graine.agents | Graine itself; Graine.DEFAULT_AGENT |
| Website | GraineAgent.load() again with another agent | — | — | One agent per page |
What to expect: a name is trimmed, and an empty one is the default agent,
"default". Configuring a name again returns that agent unchanged. Each agent
has its own session and design, chat, voice bar, actions, prompt variables set
with setVariables, booking rules and bookings, memory scope and kept
conversation. Shared by every agent: who the customer is, the install ID, the
memory choice, the current screen, the call engine — and one conversation at a
time.
Screens, actions and components
GraineScreen and GraineField
What your app reports about a screen.
GraineScreen field | Type | |
|---|---|---|
screen | String | A stable id for the screen |
title | String, or null | What the agent calls it |
journey | name, step, of, next, or null | Where the screen sits in a flow; next is the next step's name |
fields | List of GraineField | Empty by default |
error | String, or null | A banner on the screen |
data | Map | Anything else, free-form |
availableActions | List of action names, or null | Narrows what may be called here; null allows every registered action, [] offers none |
GraineField field | Type | |
|---|---|---|
name | String | |
label | String, or null | What the field is called on screen |
value | String, or null | Its real value — masked on the device before it leaves |
status | empty, filled, invalid, pending or locked, or null | invalid and empty are what make the agent speak up |
error | String, or null | The message the customer sees |
optional | Bool, default false | Left out of what counts as still to fill (isOptional in Swift) |
GraineActionResult
What an action handler answers. ActionResult on Android and in Flutter.
| Field | Type | |
|---|---|---|
status | ok, refused or error | |
message | String | For the agent, which decides how to say it |
data | Map | A structured result the agent can reason about |
Built with ok(message, data), refused(message) and error(message) on every
native platform; a plain object in TypeScript and on the website.
The component
A component the agent drew — the object your own renderer reads.
| Field | Type | |
|---|---|---|
id | String | Its id, which the answer names |
kind | String | product_list, plan_comparison, info_card, calendar_slots, quick_actions, lead_form, blank, or one of your own |
data | Object | Its content — items, slots, fields — as built in Web components, with live data merged in |
style | Object, or null | Its look: the preset and its knobs |
onSelect | path, action, args, say, or null | What the app does on a tap beyond telling the agent: open path through your navigation action, or run action with args; say replaces the sentence the agent is told. Values can carry {id}, {title}, {price}, {summary} |
fallbackText | String | What the agent would have said instead — draw it for a kind you do not render |
GraineWidget in React Native and on Android (GraineComponent is its alias on
Android); a map in Swift and Dart listeners, and in the website's messages, where
the keys are on_select and fallback_text; GraineComponent(id, kind, title, payload) in Kotlin Multiplatform, where payload is the whole component as it
arrived. A kind your build does not know is still handed over — draw its
fallbackText.
Connection values
The bar's own call — not a native call — reports these.
| Value | Words | |
|---|---|---|
| Phase | idle, connecting, reconnecting, thinking, speaking, listening | One thing for a voice bar to draw from |
| Quality | excellent, good, poor, lost, unknown | poor at 400 ms round trip, 80 ms jitter or 8 % loss; good up to 150 ms, 30 ms, 2 % |
| Connect stage | registering, dialling, retrying, switching — or none | Where a call being placed is |
| Connectivity | loading, ready, retrying, failed | Whether the engine page loaded |
| Transport | ws (standard), rtc (high quality) | What carries the bar's audio |
Each SDK spells them in its own style — GrainePhase and GraineConnectStage
in Swift (voice.connectStage), CallPhase and ConnectStage on Android and in
Flutter (voice.connectStage), ConnectionQuality everywhere. A stage an SDK
does not know is ignored, never guessed at.
Wire frames the SDK handles for you
For the curious: the SDKs read and send these themselves, and your app never has to.
The history frame
How earlier messages reach a chat when the conversation continues: one frame, before any message from the agent.
role is user or assistant, at is ISO-8601 in UTC, and channel is chat
or voice (anything else is read as chat). A turn that cannot be read — no
text, no time, another role — is skipped; a frame whose turns is not a list is
ignored. The SDKs turn each into a GraineChatTurn with
restored set.
The website loader's messages
The loader on your page talks to the widget's frame with postMessage.
| Message | Sent when | Carries |
|---|---|---|
graine:identity | identify() or setMemory() changes who is reading, and when the widget asks | value (the identifier, or null), installId, memory, timezone |
graine:window | The panel opens, or its size or layout changes (a page resized across 640 px) | size (compact, large, full), layout (window, panel, sheet, screen), and once mode (chat or call) |
graine:new-conversation | newConversation() succeeded | — |
graine:memory-forgotten | forgetMemory() succeeded | — |
The widget answers a different visitor, or nobody, by clearing its chat and starting again; a page that hosts the engine itself sends these the same way.

