Graine AI

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.

FieldType
idStringStable 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
roleThe customer, or the agentSpelled per platform — below
textStringWhat was said. An agent's turn may be only a component, with empty text
atA timeWhen it was said — for an agent's turn, its first words
deliverysending, sent or failedThe 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)
proactiveBool, default falseThe agent's turns: it spoke without being asked — its welcome, or something it noticed
liveBool, default falseThe agent's words are still arriving: draw them as provisional
componentA component, or nullWhat 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)
restoredBool, default falseAn earlier message, put back when the conversation continued. React Native: restored?: true
channelchat or voice, default chatWhere it was said. Only an earlier message can be voice: something said on a call. React Native: channel?: "voice", absent for the chat
// useGraineAgent().messages
interface Turn {
  role: "agent" | "customer"; text: string;
  live?: boolean;        // the agent is still streaming it
  pending?: boolean;     // delivery "sending": waiting for a connection
  id?: string; proactive?: boolean;
  at?: number;           // when it was said, in milliseconds since 1970 (UTC)
  restored?: true;       // an earlier message, put back when the conversation continued
  channel?: "voice";     // said on a call; 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, restored set, 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 voice turns 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.
  • live turns change. An agent's turn grows as its words arrive and settles (live false) when the turn ends, when the customer speaks, or when the connection goes. Redraw on every change; the next reply is a new turn.
  • failed waits for a tap. The SDK's chat window marks a failed message as not sent; a tap — or retry() (iOS, Kotlin Multiplatform; Android connect(), Flutter open()), the next message, or opening the chat again — sends it.
  • The transcript clears when forgetMemory() succeeds, on newConversation(), on identify(null), and when a different customer is identified on the device. A device keeps at most the last 200 turns on screen.
{
  "id": "t7",
  "role": "agent",
  "text": "Your order ships tomorrow. Want me to book a delivery slot?",
  "at": "2026-10-13T16:41:09Z",
  "delivery": "sent",
  "proactive": false,
  "live": false,
  "component": null,
  "restored": false,
  "channel": "chat"
}

An earlier message, put back when the conversation continued, and part of a call:

{ "id": "6a1f0c2e9b7d", "role": "agent", "text": "Done — I've moved your delivery to Friday.", "at": "2026-10-12T13:10:44Z", "delivery": "sent", "restored": true, "channel": "voice" }

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.

object GraineChatRows {
    fun build(turns: List<GraineChatTurn>, now: Instant = Clock.System.now(),
              zone: TimeZone = TimeZone.currentSystemDefault()): List<GraineChatRow>
    fun dayLabel(row: GraineChatRow.Day, now: Instant = Clock.System.now(), zone: TimeZone = …): String   // "Today", "Yesterday", "Sat 10 Oct"
    fun callLabel(clock: String): String                    // "Voice call · 6:05 PM"
    fun offersStarters(turns: List<GraineChatTurn>): Boolean        // nothing said yet, and nothing restored
    fun offersNewConversation(turns: List<GraineChatTurn>): Boolean // restored turns on screen
}
 
sealed class GraineChatRow {
    abstract val key: String   // stable while the conversation grows
    data class Day(val day: GraineChatDay /* TODAY, YESTERDAY, DATE */, val date: LocalDate, val at: Instant)
    data class VoiceCall(val id: String, val at: Instant, val first: Int, val last: Int)   // turns first..last, never their words
    data class Turn(val turn: GraineChatTurn, val index: Int)
}

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.

PlatformWhereValues
React NativeuseGraineAgent()connected, connecting, reconnecting, failed (booleans), typing; client.chatFailed; client.conversation: "chat", "call" or null — which conversation this agent holds now
iOSGraineChat.statusGraineChatStatus, below; online and status.isConnecting as shortcuts; typing
AndroidGraineChatconnected, connecting, reconnecting, failed (booleans), typing; also onChatState(connected, connecting) on GraineListener
FlutterGraineChatSessionconnected, connecting, reconnecting, failed (booleans), error (a sentence, or null), isOpen, agentTyping
Kotlin MultiplatformGraine.chat.statusGraineChatStatus (a StateFlow), below; typing
Website—The widget draws its own state; the page receives none

GraineChatStatus (iOS, Kotlin Multiplatform):

ValueMeans
idleNot in the chat: never opened, closed, or the app is in the background
connectingOpening
onlineConnected: a message goes now
reconnectingThe connection dropped and is being made again — iOS: reconnecting(attempt:), the attempt of six
offlineIt 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.

PlatformThe typeThe app's sizeOne open's sizeThe size in force
React NativeChatSize: "compact", "large", "full"chatSize on GraineProviderclient.openChat({ size })useGraineAgent().chatSize
iOSenum GraineChatSize: String { case compact, large, full }GraineConfig(chatSize:)chat.open(size:)chat.openedSize (an open's own, until close())
Androidenum class GraineChatSize(val wire: String) { COMPACT, LARGE, FULL }GraineConfig(chatSize = …)chat.open(size)chat.openedSize
Flutterenum GraineChatSize { compact, large, full }GraineConfig(chatSize: …)GraineChat.open(context, size: …)—
Kotlin Multiplatformenum 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.open presents the chat on the navigator — large as a sheet (a panel on a tablet), full as a screen of its own, and compact, which belongs to the bar, as large. With a bar on screen, the bar moves to the chat at that size.
  • GraineChatView, GraineChatViewController, GraineChatActivity, GraineChat and React Native's GraineChatScreen fill 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.

PlatformDeclarationReturns
React Nativeclient.newConversation(), useGraineAgent().newConversation()Promise<void>
iOSGraineChat.newConversation() async throwsnothing; throws GraineServiceError
AndroidGraineChat.newConversation(done: GraineCallback<Unit>? = null)done(Unit, null), or done(null, error) — on the main thread
FlutterGraineChatSession.newConversation()Future<void>; throws GraineException
Kotlin Multiplatformsuspend fun GraineChat.newConversation()Unit; throws GraineException
WebsiteGraineAgent.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 identifierThe 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 traitMasked on the device, then a prompt variable
What Graine keepsThe 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
memoryfalse: 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-outidentify(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 appliesFrom 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.

FieldType
idStringThe memory's id
textStringWhat is remembered, redacted before it was stored
createdAtA time, or nullWhen it was stored
updatedAtA time, or nullWhen 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.

{
  "id": "mem_4f9c21",
  "text": "Prefers a call after 6 pm on weekdays.",
  "createdAt": "2026-10-02T13:05:11Z",
  "updatedAt": null
}

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.

3

Session settings

What the SDK reads from the agent's settings once per launch.

ValueType
identity fieldString, default user_idThe trait that names the customer — user_id unless the agent's settings choose email, phone or a custom trait
devices per userInt, 1–5, default 2How many installs can act as one customer
memory enabledBool, or nullThe agent remembers its customers; null from an older server
hosted callsBool, platforms, ring timeout, call nameGraine rings this workspace's apps, and how
call audio"webrtc", "websocket", or absentThe 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 rulesGraineSchedulingRulesWhat customers may book
PlatformWhere
React Nativeclient.config: identity (field, devicesPerUser), memory (enabled), calls (hosted, platforms, ringTimeoutSeconds, callName, scheduling); client.schedulingRules
iOSGraineInApp.session (GraineSessionConfig): identity (GraineIdentitySettings: field, devicesPerUser), memoryEnabled, calls (GraineHostedCallsSettings: hosted, platforms, ringTimeoutSeconds, callName, usersCanBook, … and ringsThisPlatform), schedulingRules
AndroidGraineInApp.get().session and onSessionReady(session), a SessionConfig: identity (SessionIdentity: field, devicesPerUser), memoryEnabled, schedulingRules (with ringsAndroid)
FlutterGraineInApp.session (SessionConfig): identity and identitySetting (field, devicesPerUser), memory (enabled), calls (hosted, platforms, ringTimeoutSeconds, callName, scheduling); schedulingRules
Kotlin MultiplatformGraine.session (a StateFlow<GraineSession?>): agentId, identityField, devicesPerUser, memoryEnabled, schedulingRules, declaredActions
WebsiteGraineAgent.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.

{
  "identity": { "field": "user_id", "devicesPerUser": 2 },
  "memoryEnabled": true,
  "calls": { "hosted": true, "platforms": ["ios", "android"], "ringTimeoutSeconds": 45, "callName": "Asha from Acme" },
  "schedulingRules": { "usersCanBook": true, "minLeadMinutes": 5, "maxDaysAhead": 14, "maxPendingPerUser": 3 }
}

IdentityChannel

React Native's identityChannel(): what the SDK will send with the next conversation.

FieldType
valueString, or nullThe identifier, as it is sent — never a prompt variable. Null when nobody is identified
installIdString, or nullThis install. Null when there is nowhere to keep one
memoryBoolThe customer's memory choice
timezoneString, or nullThe device's time zone, IANA
{ "value": "u_8812", "installId": "3f6a0c1e-9b2d-4c7a-8e51-2d9f0a7b6c43", "memory": true, "timezone": "Asia/Kolkata" }

Scheduling

GraineScheduledCall

A call booked for later — returned by scheduleCall and scheduledCalls, and shown in the chat's upcoming calls.

FieldType
idStringGraine's id for the booking — what cancelScheduledCall takes
atA time, or nullWhen it rings. Null only if Graine's time for it could not be read
statusGraineCallStatusWhere it is; scheduled when Graine says nothing
sourceGraineCallSource, or nullWho booked it; null when Graine does not say
reasonString, or nullWhat it is about, up to 120 characters
isUpcomingBoolstatus is scheduled or ringing
canCancelBoolstatus is scheduled
interface GraineScheduledCall {
  id: string; at: Date | null; status: GraineCallStatus; source: GraineCallSource | null;
  reason: string | null; isUpcoming: boolean; canCancel: boolean;
}

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.

{
  "id": "c3a8f1d2-5b7e-4a19-9f02-6d4e8b1a7c35",
  "at": "2026-10-14T12:30:00Z",
  "status": "scheduled",
  "source": "app",
  "reason": "Talk through my order",
  "isUpcoming": true,
  "canCancel": true
}

GraineCallStatus

Where a booked call is. The dashboard and your webhooks use the wire words; the SDKs use the enum.

scheduled → ringing → answered → completed
    │           ├──→ declined
    │           ├──→ missed      (rings once more after your re-ring delay, if set)
    │           └──→ failed
    ├──→ cancelled
    ├──→ expired
    └──→ no_device
StatusWireMeans
scheduledscheduledBooked, waiting for its time — or held a little while for credit or a switched-off agent
ringingringingRinging on the customer's device now
answeredansweredPicked up; the conversation is on
completedcompletedThe conversation ended
declineddeclinedThe customer declined it. Nothing rings them again for at least 6 hours, and a declined call never re-rings
missedmissedNobody answered within the ring time
failedfailedIt could not ring or connect
cancelledcancelledCalled off by the customer, your app or your team, before it rang
expiredexpiredIt 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
noDeviceno_deviceNo app device of the customer's was seen in the last 30 days, so there was nothing to ring
unknownanything elseA 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.

SourceWire
agentagentThe agent, during a conversation
appsdkYour app or website, through scheduleCall
dashboarddashboardYour team, in the dashboard
apiapiThe API
unknownanything elseA 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.

FieldTypeDefault
usersCanBookBoolfalseUsers, from your app is on. false when the agent's settings say nothing
minLeadMinutesInt5The soonest a booking may be
maxDaysAheadInt14The furthest ahead, in days
maxPendingPerUserInt3Calls a customer may have waiting
ringTimeoutSecondsInt45How long a ring Graine sends lasts
callNameString, or nullnullWhat the customer's phone shows when it rings; null: your app's own name
hostedRingingBoolfalseGraine rings this workspace's apps (the push credentials are uploaded)
platformsList of ios, androidemptyWhere Graine rings; empty means every platform
earliest(now)A timenow plus minLeadMinutes
latest(now)A timenow plus maxDaysAhead days
interface GraineSchedulingRules {
  usersCanBook: boolean; minLeadMinutes: number; maxDaysAhead: number; maxPendingPerUser: number;
  ringTimeoutSeconds: number; callName: string | null; hostedRinging: boolean; platforms: ("ios" | "android")[];
  earliest(now?: Date): Date; latest(now?: Date): Date;
}

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.

{
  "usersCanBook": true,
  "minLeadMinutes": 5,
  "maxDaysAhead": 14,
  "maxPendingPerUser": 3,
  "ringTimeoutSeconds": 45,
  "callName": "Asha from Acme",
  "hostedRinging": true,
  "platforms": ["ios", "android"]
}

What the scheduling calls return

CallReturnsWhen it resolves
scheduleCall(at, reason)A GraineScheduledCall, status scheduledOnce Graine has booked it
scheduledCalls()A list of them, soonest first; empty when there are noneOne request
cancelScheduledCall(id)Its GraineCallStatus afterwards: cancelledOnce 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.

FieldType
callIdStringYour server's call_id, Graine's id for a call it rang, or the id startCall returned
outcomeringing, answered, declined, missed, ended or failedWhat happened
reasonString, or nullWith 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
durationSecondsInt, or nullWith ended: how long it was connected, in whole seconds
hostedBool, default falseGraine rang it, rather than your own server
busyBool, default falseAndroid: a missed ring refused because another call was live
agentString, or nullThe 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
type GraineCallOutcome = (
  | { callId: string; outcome: "ringing" } | { callId: string; outcome: "answered" }
  | { callId: string; outcome: "declined" } | { callId: string; outcome: "missed" }
  | { callId: string; outcome: "ended"; reason: "local" | "remote" | "failed" }
  | { callId: string; outcome: "failed"; error: string }
) & { durationSeconds?: number | null; hosted?: boolean; agent?: string | null };

What to expect

  • Every outcome carries the call's id; send it to your server with it. An answered call is followed by ended or failed. A call that connected and dropped ends ended with failed — calls do not reconnect.
  • What each platform reports. Kotlin Multiplatform reports every field above but busy (its reason says busy), ringing included; on Android its agent is null for a ring refused because another call was live. iOS reports the five outcomes, with endReason and error; read GraineCalls.agent and GraineCalls.hosted for the call. Android has no ringing (a ring is onIncomingCall) and no agent (calls.callAgent). React Native and Flutter type every field; their 0.7.0 engines send callId, outcome, reason (or error) and agent, and leave durationSeconds and hosted empty and ringing unsent.
  • 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.
{
  "callId": "c3a8f1d2-5b7e-4a19-9f02-6d4e8b1a7c35",
  "outcome": "ended",
  "reason": "local",
  "durationSeconds": 212,
  "hosted": true,
  "agent": "support"
}

GraineIncomingCall

A call ringing on this device.

FieldType
callIdStringThe call's id
callerNameString, or nullWhat the ring shows: the push's caller name, else your calls configuration's display name
reasonString, or nullThe push's reason — never in a ring Graine sends, whose reason reaches the agent when it connects
variablesMap of stringsThe push's prompt variables (iOS variables; Android GraineCall.variables)
hostedBoolGraine rang it (iOS, Android, Kotlin Multiplatform, React Native — hosted?: boolean, present when true; Flutter's field stays false with the 0.7.0 engines)
agentString, or nullThe 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
PlatformDeclared asHeard through
React Nativeinterface GraineIncomingCall { callId; reason; callerName; agent?; hosted? }GraineCalls.onIncomingCall(fn)
iOSstruct GraineIncomingCall { callId; reason; callerName; variables; hosted; agentId }GraineCallsListener.onIncomingCall(_:)
Androiddata class GraineCall(callId, direction, callerName, reason, variables, hosted)GraineCallListener.onIncomingCall(call)
Flutterclass GraineIncomingCall { callId; reason; callerName; hosted; agent }GraineCalls.instance.incomingCalls
Kotlin Multiplatformdata class GraineIncomingCall(callId, callerName, reason, hosted, agent)Graine.calls.incoming
Website——
{ "callId": "c3a8f1d2-5b7e-4a19-9f02-6d4e8b1a7c35", "callerName": "Asha from Acme", "reason": null, "hosted": true, "agent": "support" }

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.

FieldType
callIdStringThe missed call
callerNameString (Kotlin Multiplatform: or null)What the ring showed
reasonString, or nullIts reason, when the push had one
hostedBoolGraine rang it
atA timeWhen it was missed
agentString, or nulliOS: 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.

{ "callId": "c3a8f1d2-5b7e-4a19-9f02-6d4e8b1a7c35", "callerName": "Asha from Acme", "reason": null, "hosted": true, "at": "2026-10-14T12:31:00Z", "agent": "support" }

The call, to draw

What a call screen of your own reads.

FieldType
stateidle, ringing, connecting, active or endedWhere the call is
callIdString, or nullNull at idle
directionincoming or outgoing, or nullWho rang whom
reasonString, or nullThe push's reason, or the one startCall was given
callerNameString, or nullWhat the call shows (iOS: an empty string for none)
mutedBool
speakerOnBoolspeaker on Android
agentSpeakingBoolThe agent's voice is playing
captionGraineCaption, or nullThe line being said
connectedAtA time, or nullWhen the conversation opened — for a timer
errorString, or nullWhy the last call could not connect (React Native, Flutter); cleared when the next call starts
hostedBoolGraine rang it (iOS, Kotlin Multiplatform)
agentString, or nullThe name of the agent the call is with (React Native, iOS, Flutter, Kotlin Multiplatform; Android: callAgent, the agent itself); null at idle
transportwebrtc or websocket, or nullWhat carries the call's audio, once decided — GraineCallTransport. Null before
transportFallbackReasonString, or nullWhy a call that tried WebRTC runs on the WebSocket — the reasons. Null otherwise
PlatformDeclared as
React NativeGraineCallSnapshot — useGraineCall(), GraineCalls.getSnapshot(). connectedAt is milliseconds since 1970
iOSPublished properties on GraineCalls (state is GraineCallState, with ended(reason)), plus transcript, missedCall and showsCallScreen
AndroidProperties 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
FlutterGraineCallSnapshot — GraineCalls.instance.value; its state field is status, a GraineNativeCallStatus
Kotlin MultiplatformGraineCallInfo — 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.

{
  "state": "active",
  "callId": "c3a8f1d2-5b7e-4a19-9f02-6d4e8b1a7c35",
  "direction": "incoming",
  "reason": null,
  "callerName": "Asha from Acme",
  "muted": false,
  "speakerOn": false,
  "agentSpeaking": true,
  "caption": { "role": "agent", "text": "Thanks for picking up — is now a good time?", "live": true },
  "connectedAt": "2026-10-14T12:30:04Z",
  "error": null,
  "agent": "support",
  "transport": "webrtc",
  "transportFallbackReason": null
}

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).

ValueAs callTransportAs transport
autoThe default: WebRTC when the app includes the WebRTC module and the agent's In-app call audio is not Standard; else the WebSocket—
websocketAlways the WebSocketThe call's audio runs on the WebSocket
webrtcWebRTC wherever it can carry the call; the WebSocket when the module is missing or WebRTC cannot set this call upThe call's audio runs on WebRTC
PlatformDeclared asChosen inRead on
React Nativetype GraineCallTransport = "auto" | "websocket" | "webrtc"GraineCalls.configure({ callTransport })GraineCallSnapshot.transport, transportFallbackReason
iOSenum GraineCallTransport: String { case auto, websocket, webrtc }GraineCallsConfig(callTransport:)GraineCalls.transport (GraineCallTransport?), transportFallbackReason
Androidenum class GraineCallTransport { AUTO, WEBSOCKET, WEBRTC }GraineCallsConfig(callTransport = …)GraineCalls.transport ("webrtc" / "websocket"), transportFallbackReason
Flutterenum GraineCallTransport { auto, websocket, webrtc }GraineCallsOptions(callTransport: …)GraineCallSnapshot.transport, transportFallbackReason
Kotlin Multiplatformenum 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.

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, when the call would have used WebRTC: the agent's In-app call audio is Standard
unavailableAndroid, when the app asked for webrtc: this build has no WebRTC module, or the device cannot run it

GraineCaption

FieldType
roleThe agent or the customeragent / customer (USER / AGENT in Kotlin Multiplatform)
textStringThe line
liveBoolStill 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:

PlatformNull when
React NativeA call is already live. It rejects for an agent name configure did not name
iOSA call is already live, or no agent has that name
AndroidA call is already live, or no agent has that name. Call it on the main thread, from an activity in front
FlutterA call is already live, or the build has no native calls
Kotlin MultiplatformA 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
typeagent_call — anything else is your own message, and handlePush returns false
call_idThe call's id; required
caller_nameWhat the ring shows
hosted"1" when Graine rang it
agent_idThe agent the call is with, when it names one: the app rings with that configured agent
reason, variablesOnly 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.

{ "type": "agent_call", "call_id": "c3a8f1d2-5b7e-4a19-9f02-6d4e8b1a7c35", "caller_name": "Asha from Acme", "hosted": "1", "agent_id": "agent_5b7d21e9c8a34f60" }

Errors

GraineError

What a method that asks Graine fails with.

FieldType
codeGraineErrorCodeWhy — branch on this, never on the message
messageStringA sentence written for the customer; nothing personal in it
retryAfterSecondsInt, or nullWith rate_limited: how long Graine asked to wait, when it said
statusIntThe HTTP status, 0 when nothing answered (React Native, iOS, website; statusCode, or null, in Dart)
PlatformDeclared as
React Native, websiteclass GraineError extends Error { code; status; retryAfterSeconds }
iOSstruct GraineServiceError: LocalizedError { code: GraineErrorCode; message; retryAfterSeconds; status }
Androiddata class GraineError(code: GraineErrorCode, message: String, retryAfterSeconds: Int? = null), in the callback — never thrown
Flutterclass GraineException { message; code: GraineErrorCode; codeName; statusCode; retryAfterSeconds }
Kotlin Multiplatformclass 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.

{ "code": "too_soon", "message": "Pick a time at least 5 minutes from now", "retryAfterSeconds": null, "status": 0 }

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.

PlatformConfigureLook upEvery agentThe default
React NativeOne GraineProvider (or client) per agent; the calls package names them: GraineCalls.configure({ agents })—GraineCalls.agentsThe root provider; DEFAULT_AGENT ("default") in the calls package
iOSGraineInApp.configure(_:name:)GraineInApp.named(_:), nil when not configuredGraineInApp.allGraineInApp.shared, GraineInApp.current
AndroidGraineInApp.init(application, config, name)GraineInApp.find(name), null when not configured; get(name) throwsGraineInApp.allGraineInApp.get(); GraineInApp.DEFAULT
FlutterGraineInApp.init(config, name:)GraineInApp.named(name), null when not configuredGraineInApp.allGraineInApp.instance, maybeInstance; GraineInApp.defaultName
Kotlin MultiplatformGraine.configure(config, name) returns a GraineAgentGraine.agent(name), null when not configuredGraine.agentsGraine itself; Graine.DEFAULT_AGENT
WebsiteGraineAgent.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 fieldType
screenStringA stable id for the screen
titleString, or nullWhat the agent calls it
journeyname, step, of, next, or nullWhere the screen sits in a flow; next is the next step's name
fieldsList of GraineFieldEmpty by default
errorString, or nullA banner on the screen
dataMapAnything else, free-form
availableActionsList of action names, or nullNarrows what may be called here; null allows every registered action, [] offers none
GraineField fieldType
nameString
labelString, or nullWhat the field is called on screen
valueString, or nullIts real value — masked on the device before it leaves
statusempty, filled, invalid, pending or locked, or nullinvalid and empty are what make the agent speak up
errorString, or nullThe message the customer sees
optionalBool, default falseLeft out of what counts as still to fill (isOptional in Swift)
{
  "screen": "delivery_address",
  "title": "Delivery address",
  "journey": { "name": "Checkout", "step": 2, "of": 3, "next": "Payment" },
  "fields": [
    { "name": "postal_code", "label": "Postal code", "value": "56007", "status": "invalid", "error": "We don't deliver there yet" }
  ]
}

GraineActionResult

What an action handler answers. ActionResult on Android and in Flutter.

FieldType
statusok, refused or error
messageStringFor the agent, which decides how to say it
dataMapA 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.

{ "status": "refused", "message": "The order has already shipped, so the address can't change.", "data": {} }

The component

A component the agent drew — the object your own renderer reads.

FieldType
idStringIts id, which the answer names
kindStringproduct_list, plan_comparison, info_card, calendar_slots, quick_actions, lead_form, blank, or one of your own
dataObjectIts content — items, slots, fields — as built in Web components, with live data merged in
styleObject, or nullIts look: the preset and its knobs
onSelectpath, action, args, say, or nullWhat 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}
fallbackTextStringWhat 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.

{
  "id": "w_19",
  "kind": "calendar_slots",
  "data": { "slots": [{ "id": "2026-10-17T10:00", "label": "Fri 17 Oct, 10:00" }, { "id": "2026-10-17T15:30", "label": "Fri 17 Oct, 15:30" }] },
  "style": { "preset": "compact" },
  "onSelect": { "path": "appointments?time={id}" },
  "fallbackText": "I can offer Friday at 10:00 or 15:30."
}

Connection values

The bar's own call — not a native call — reports these.

ValueWords
Phaseidle, connecting, reconnecting, thinking, speaking, listeningOne thing for a voice bar to draw from
Qualityexcellent, good, poor, lost, unknownpoor at 400 ms round trip, 80 ms jitter or 8 % loss; good up to 150 ms, 30 ms, 2 %
Connect stageregistering, dialling, retrying, switching — or noneWhere a call being placed is
Connectivityloading, ready, retrying, failedWhether the engine page loaded
Transportws (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.

{
  "type": "history",
  "turns": [
    { "id": "6a1f0c2e9b7d", "role": "user", "text": "Can you move my delivery?", "at": "2026-10-12T13:10:31Z", "channel": "voice" },
    { "id": "6a1f0c2e9b7e", "role": "assistant", "text": "Done — I've moved your delivery to Friday.", "at": "2026-10-12T13:10:44Z", "channel": "voice" }
  ],
  "continued": true
}

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.

MessageSent whenCarries
graine:identityidentify() or setMemory() changes who is reading, and when the widget asksvalue (the identifier, or null), installId, memory, timezone
graine:windowThe 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-conversationnewConversation() succeeded—
graine:memory-forgottenforgetMemory() 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.