Graine AI

Calls

Native calls on every platform — configure and its options, push tokens and handing over pushes, startCall and callBack, answering, ending, muting and the loudspeaker, the call's state, outcomes and incoming calls, attach — each with parameters, returns, threading and side effects.

How calls work, the setup per platform and the test plan are in In-app calling; the push format, limits and failure messages in the calling reference. The website takes no native calls. Where the calls object lives: GraineCalls (React Native, from @graineai/inapp-react-native-calls), GraineInApp.shared.calls (iOS), GraineInApp.get().calls and the GraineCalls companion (Android), GraineCalls.instance (Flutter), Graine.calls (Kotlin Multiplatform).

configure

How calls ring and what they show. Once, at launch.

GraineCalls.configure(options: GraineCallsOptions): Promise<void>
GraineCalls.configure(options: GraineCallsAgentsOptions): Promise<void>        // agents only, no top-level key
GraineCalls.configure(name: string, options: GraineCallsAgentOptions): Promise<void>   // one more agent
GraineCalls.agents: string[]                                                   // the configured names, "default" among them
 
await GraineCalls.configure({ publishableKey: "pk_live_…", displayName: "Asha from Acme" });
OptionTypeDefault
publishableKeystringrequiredThe agent's key
displayNamestringrequiredThe ring and the call screen
baseUrlstringhttps://www.graine.ai
ringtoneSoundstringthe phone's ringtoneiOS: a sound file of 30 s or less in the bundle. Android: a raw resource name
ringTimeoutMsnumber30000How long a ring lasts
appVersionstringLands on the call record
callScreen"sdk" or "app""sdk"Who draws the call while your app is open
debugbooleanfalseThe native call log; read at launch
android.smallIconstringthe platform's call iconA drawable name
ios.iconTemplate, ios.includesCallsInRecentsstring, boolean—, false
callTransport"auto", "websocket" or "webrtc""auto"What carries the calls' audio — Calls over WebRTC. Case-sensitive; anything else rejects with invalid_options
agentsRecord<string, { publishableKey: string; baseUrl?: string }>Multi-agent API. More agents, by name. Without a top-level publishableKey, the first one listed is the default agent. Each call's name, ring and call-screen design come from its agent's own settings (the name falls back to displayName); the other options are shared

Rejects with code invalid_options and a message naming the option. Kept on the device, so a push that wakes a closed app rings before JavaScript runs; a new key or base URL applies at the next launch.

Push tokens and pushes

The token a push reaches this device with, and handing each push to the SDK so it rings. With hosted ringing, the SDK registers the token with Graine itself. On Android, with Firebase Cloud Messaging in the app, the SDK's own messaging service receives the token and every push, and the SDK reads the token when it starts: the calls below are for an app with a messaging service of its own, which outranks the SDK's.

GraineCalls.enableVoIPPushes(onToken: (token: string | null) => void): () => void      // iOS; returns: stop updates
GraineCalls.registerPushToken(push: { token: string; kind?: "fcm" | "voip"; environment?: "production" | "sandbox" }): Promise<boolean>
GraineCalls.handlePush(data: Record<string, string>): Promise<boolean>
 
// Android, only with @react-native-firebase/messaging:
messaging().onTokenRefresh((token) => GraineCalls.registerPushToken({ token }));
messaging().setBackgroundMessageHandler(async (m) => { await GraineCalls.handlePush(m.data ?? {}); });

registerPushToken resolves true when the device was registered, false on iOS (which registers its VoIP token itself), and rejects on a build whose native module predates it. handlePush resolves true for any call push.

startCall

The customer calls the agent.

GraineCalls.startCall(options?: { reason?: string; variables?: Record<string, unknown>; agent?: string }): Promise<string | null>
 
const id = await GraineCalls.startCall({ reason: "order_help" });

Rejects for an agent that is not configured.

ParameterTypeLimits
reasonStringBecomes call_reason; sent as it is, so make it an identifier
variablesMap12 variables in all with who_called and call_reason; names 40 characters, values 200, about 1 KBMasked on the device
agentStringThe name of a configured agentMulti-agent API. Places the call with that agent — its key, its actions, its design on the call screen; the default agent when omitted. React Native: a name from configure's agents — several agents

Returns the call's id — call_ and a lowercase UUID — or nothing while another call is live. Side effects the microphone is asked for if missing; a conversation in the bar ends. Outcomes follow through the outcome listener.

"call_5e0f2a9c-7d41-4b6e-9a3c-1f8d2b7e6a05"

callBack and the missed call

PlatformMemberReturns
iOSmissedCall: GraineMissedCall? (published), callBack() -> String?, dismissMissedCall(), showsCallScreen: BoolcallBack calls back the agent that rang, with the missed call's reason; the new call's id, or nil
AndroidcallBack(activity: Activity, missedCallId: String?, reason: String? = null): String?From the missed-call notification's Call back, with the agent that rang; constants ACTION_MISSED, ACTION_CALL_BACK, EXTRA_CALLER_NAME, EXTRA_REASON, EXTRA_AGENT
Kotlin Multiplatformstate.value.missedCall, callBack(): String?, dismissMissedCall()

A missed ring stays on the call screen for 30 seconds on iOS (GraineCallView shows Missed call with Close and Call back; no notification is left, and only includesCallsInRecents lists it in the Phone app's Recents). On Android it leaves a missed-call notification with Call back, and a call screen that showed the ring stays on Missed call for 30 seconds. GraineMissedCall

Answering, ending, muting, the loudspeaker

React NativeiOSAndroidFlutterKotlin Multiplatform
Answeranswer(): Promise<void>answer()answer()answer(): Future<void>answer()
Declinedecline(): Promise<void>end() while ringingdecline()decline(): Future<void>decline()
Hang upend(): Promise<void>end()end()end(): Future<void>end()
MutesetMuted(muted)setMuted(_:)setMuted(value)setMuted(muted)setMuted(muted)
LoudspeakersetSpeaker(on)setSpeaker(_:)setSpeaker(on)setSpeaker(on)setSpeaker(on)

For a call screen of your own; the SDK's screens do this themselves. Main thread on iOS and Android. A call starts on the earpiece, or a connected headset.

The call's state

GraineCalls.getSnapshot(): GraineCallSnapshot
GraineCalls.subscribe(fn: () => void): () => void
useGraineCall(): GraineCallSnapshot
GraineCalls.isAvailable: boolean       // the native module is linked
// snapshot.transport: "webrtc" | "websocket" | null; snapshot.transportFallbackReason: string | null

Fields and a JSON example: The call, to draw. On iOS ended lasts one second before idle; elsewhere it can pass at once — treat ended and idle alike and take how the call ended from its outcome.

Calls over WebRTC

A native call — one the customer places and one Graine rang — carries its audio over WebRTC when the app includes the SDK's WebRTC module, and over the WebSocket otherwise. WebRTC adds a jitter buffer, loss concealment and the platform's echo canceller: on a weak or changing network the agent's voice holds up instead of stalling. Nothing else changes — the ring, the call screen, the states and the outcomes are the same — and the call is prepared at the tap, or while a ring is showing, so it connects as fast as before.

# ios/Podfile, at the top — then pod install
ENV["GRAINE_WEBRTC"] = "1"
# android/gradle.properties
graineWebRtc=true
await GraineCalls.configure({ publishableKey: "pk_live_…", displayName: "Asha from Acme", callTransport: "auto" });
const { transport, transportFallbackReason } = useGraineCall();

On a build server (EAS, CI), GRAINE_WEBRTC=1 in the build's environment turns it on for both. Rebuild the app: it is native code.

callTransport
auto (the default)WebRTC when the module is in the app; else the WebSocket
websocketAlways the WebSocket
webrtcWebRTC wherever it can carry the call; the WebSocket when the module is missing or WebRTC cannot set this call up

The dashboard decides last. The agent's In-app call audio setting (agent → Calls & scheduling) is WebRTC (recommended) or Standard. Standard keeps every install's calls on the WebSocket, whatever the app chose, from each app's next start — no release needed.

When WebRTC cannot set a call up before any audio flows — a network that blocks it, a refusal, no answer within 12 seconds — the call continues on the WebSocket inside the same call: no second ring, no new call screen, no error; the customer sees Connecting… a moment longer. A call the agent cannot take at all (no credit left, the agent switched off) ends with that reason, as it does without the module. Once the audio flows, a call never changes road: a dropped connection gets one chance to recover, then the call ends.

Which road a call took: transport (webrtc or websocket) and, for a call that tried WebRTC and runs on the WebSocket, transportFallbackReason — final once the call is active. The values: GraineCallTransport.

Sound and size. 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. The module adds a prebuilt WebRTC library (Google's WebRTC, BSD 3-Clause, its symbols renamed so it never clashes with another copy in your app): about 12 MB installed on an iPhone (6 MB to download), about 7.4 MB on a 64-bit Android phone. List it in your app's open-source licences.

Outcomes and incoming calls

OutcomesIncoming calls
React NativeGraineCalls.onOutcome(fn): () => voidGraineCalls.onIncomingCall(fn): () => void
iOSGraineCallsListener.onCallOutcome(_:callId:)onIncomingCall(_:) — listeners added with addListener, held weakly
AndroidGraineCallListener.onCallOutcome(callId, outcome)onIncomingCall(call) — main thread, kept until removed
FlutterStream<GraineCallOutcome> outcomesStream<GraineIncomingCall> incomingCalls
Kotlin MultiplatformSharedFlow<GraineCallOutcome> outcomesSharedFlow<GraineIncomingCall> incoming

React Native and Flutter hold the last 20 outcomes and rings and hand them to the first listener, oldest first — for an app a push launched. Payloads: GraineCallOutcome, GraineIncomingCall. Every calls listener, with its threading: Events and listeners → Calls listeners.

attach

React Native and Flutter: hand a call what your app tells the agent — who the customer is, prompt variables, the current screen, your actions and the memory choice — and keep it in step.

GraineCalls.attach(client: GraineInAppClient, options?: { agent?: string }): () => void    // React Native; returns: detach
void Function() attach([GraineInApp? graine])                 // Flutter; returns: detach

With several agents: in React Native, attach each provider's client — it is matched to the configured agent with the same key, or name it with agent. In Flutter, attach the default agent; while a call is with another agent, the engine holds that agent's variables and actions and runs them in its handlers.

Without it, the agent's action requests during a call are refused. In React Native it also ends a conversation in the bar when a call goes live and keeps the bar from starting until the call is over. iOS, Android and Kotlin Multiplatform do this by themselves.

On this page