Graine AI

Start, switch and end a session

When the SDK connects, how the call and the chat take turns, why a session ends when the app is put away, and what to check when nothing appears.

Getting the SDK in for the first time is the Quickstart. This page is what happens around it: when it connects, how long a session lives, and what to look at when nothing shows up.

Two dashboard steps, both easy to miss

  1. Give the app its own agent. Clone your website agent rather than pointing the app at it — actions are declared per agent and the model is offered all of them on every surface that agent serves. (Why)
  2. Add app:// to the allowlist. A native app sends no Origin header, so the allowlist has nothing to match. Without this entry the key is rejected and the launcher renders nothing at all.

Connect at launch, not on tap

GraineProvider connects when your app starts. Leave it that way.

An agent that connects only when the customer taps can never be the one to speak first — and speaking first, at the moment somebody stalls, is most of what an in-app agent is for.

Connecting is cheap and silent: nothing is shown, no microphone is opened, and no conversation is billed until there is one.

The call and the chat

Your agent can be a call, a chat, or both — chosen in Embed & Widgets → Appearance → What visitors get, alongside Opens on (Chat). The SDK reads both, so the same settings that shape your website widget shape your app, without a release.

SettingIn the app
Call and chatThe bar opens on the call and carries a switch to the chat
Voice onlyThe call. No switch is drawn
Chat onlyThe chat. No microphone is opened and no call engine is mounted

Exactly one of them runs at a time. They are different sockets — a chat has no speech synthesis at all — so the SDK ends the call when the customer moves to the chat, and closes the chat when they move back. Without that the agent answers twice: once aloud and once in writing.

const { surface, canSwitch, setSurface } = useGraineSurface();
// surface: "voice" | "chat"

The SDK's bar — GraineVoiceBar on a call, GraineAgentBar for chat alone — draws the switch for you when the dashboard allows both. Drawing your own, or opening on the chat for one screen:

<GraineProvider … surface="chat" onSurfaceChange={(s) => log(s)}>

The prop chooses among the surfaces the dashboard allows; it cannot add one that was turned off. An app that hard-codes surface="voice" must not be able to put a microphone in front of the customers of a business that chose chat only.

autoConnect={false} is the old advice for apps using GraineVoiceLauncher, and it still works. It is no longer needed: the launcher tells the SDK it owns the call, and the provider's own socket is opened only for the chat.

The session follows the app

One session per client, and it ends when the app leaves the foreground.

Backgrounding does not unmount a React tree, so without this the WebView stays mounted with the microphone open and the conversation still billing while nobody is listening. iOS is the worse case: it suspends the WebView's audio without telling the socket, so the call survives as a leg that can no longer hear anything and bills for silence.

<GraineProvider … endOnBackground={false}>   {/* default: true */}

It governs the call as well as the chat (the voice launcher honours it from 0.40.1; before, a call always ended). Turn it off only when your app keeps the call alive itself: a call screen of its own and, on Android, a microphone foreground service. On iOS, test it on real devices first — whether a web view keeps its microphone once the app leaves the screen varies between iOS versions, and a call that loses it carries on without the customer's voice. See In-app calling → Talking with the screen off.

It ends on background, never on inactive. iOS reports inactive for the app switcher, a pulled-down notification, and the moment a permission dialog appears — ending on that would mean asking for the microphone hangs up the call that asked for it.

Chat sessions reconnect when the app returns. Voice does not auto-resume: a call is something a person chose to start, and starting one again because they came back is a microphone opening unasked. connected goes false — draw your UI from that.

connect() is idempotent, so calling it twice returns the same session rather than opening a second socket. close() sends a stop frame before dropping the socket, so the conversation is filed as ended rather than dropped.

Bring your own UI

You rarely need to. The brand's look is chosen in the dashboard — a design, its colour, corners, typeface, launcher and motion — and the SDK's bar draws exactly that on every phone, so a brand team works in the Garage, not in your code. Replace the bar and the design chosen there stops reaching your app.

If you must, everything the SDK draws is built on the same public hooks you have, and useGraineTheme() hands you the design's tokens to match:

const { connected, messages, send } = useGraineAgent();
const theme = useGraineTheme();   // colours, type, corners, spacing, shadows

The call's own UI — every state to draw, and the code for React Native, Android, iOS, Flutter and a website — is in Your own UI.

When nothing appears

Errors are logged loudly and shown to nobody: a misconfigured agent renders no launcher at all, rather than a launcher that opens onto an error.

What you seeWhyFix
No launcher, no_origin in consoleThe allowlist has no app:// entryAdd app:// under Embed & Widgets
No launcher, no_domains_configuredThe allowlist is empty, which denies everythingAdd app://, plus any web origins that agent serves
No launcher, error in consoleThe key was rejectedCopy the key again from Embed & Widgets
The agent offers things the app cannot doThe website agent is being reusedClone it and give the app its own
Launcher, but no replyThe agent has no web-call variantRe-save the agent to create one
Replies in text during a voice sessionConnected without voiceAdd the voice prop, or use GraineVoiceLauncher
No microphone, and no way to get oneThe agent is set to chat onlyEmbed & Widgets → Appearance → What visitors get
The switch is missingThe agent is one surface, not bothSame setting — choose "Call and chat"
Nothing at all, no console outputonError was not passedPass it — this is the only way some failures surface

A conversation that worked appears in Call history in the dashboard within a few seconds.

Next steps

On this page