Changelog
Every released version of the in-app SDKs, what changed, and whether it asks anything of you.
React Native is published as
@graineai/inapp-react-native:
an upgrade is npm install and an over-the-air update — no native rebuild.
Android, iOS, Flutter and Kotlin Multiplatform versions are listed alongside. Improvements to the
hosted voice engine reach every app without an update; those entries say
live, no upgrade.
Upgrading from anything older than 0.27.4
Go straight to the latest React Native version. Two releases in that range were
never published, and 0.27.0 must not be installed.
iOS and Android 0.14.11, Kotlin Multiplatform 0.14.12 — a call whose audio would not start recovers
5 November 2026. Flutter and React Native stay at 0.14.4 (React Native 0.47.3, calls 0.8.3). Android has no change of its
own. On iOS, update GraineInAppRTC beside GraineInApp: the change is in it. Kotlin Multiplatform 0.14.12 builds on
ai.graine:inapp-android 0.14.11 and, on iOS, GraineInApp 0.14.11. Asks nothing else of you.
- A call whose audio would not start now recovers. On a WebRTC call on an iPhone, the phone's audio unit could fail to
start after CallKit had activated the call's audio. WebRTC does not try again by itself, and the SDK's restart could not
bring it back, so the call connected, nothing played, the agent heard nothing, and the call ended after about 5 seconds
with "The call's audio could not start." The SDK now asks for it again at once, up to five times, while the agent's
greeting waits for the audio. In Execution Logs, the
audioline saysunit_failedwhen it was needed.
iOS and Android 0.14.10, Kotlin Multiplatform 0.14.11 — the app says when Xcode links an older iOS SDK
5 November 2026. Flutter and React Native stay at 0.14.4 (React Native 0.47.3, calls 0.8.3). Android has no change of its
own. On iOS, update GraineInAppRTC beside GraineInApp (no change of its own). Kotlin Multiplatform 0.14.11 builds on
ai.graine:inapp-android 0.14.10 and, on iOS, GraineInApp 0.14.10.
- Kotlin and Xcode can drift apart, and now you will hear about it.
inapp-kmpandGraineInAppare separate dependencies, so updating Kotlin does not update the iOS SDK, and the app builds and runs either way. The older SDK's bugs stay with it, such as calls that connect with no sound.callSetupProblemsnow reportsios_sdk_olderwith both versions and the fix, and the same sentence goes to the Xcode console at launch, with or withoutdebug.
iOS and Android 0.14.9, Kotlin Multiplatform 0.14.10 — the calls setup check names the app, not the agent
4 November 2026. Flutter and React Native stay at 0.14.4 (React Native 0.47.3, calls 0.8.3), the website loader at
1.6.1. On iOS, update GraineInAppRTC beside GraineInApp, and on Android inapp-android-rtc beside inapp-android
(no change of their own). Kotlin Multiplatform 0.14.10 builds on ai.graine:inapp-android 0.14.9 and, on iOS,
GraineInApp 0.14.9. Asks nothing of you.
hosted_ringing_offnames the app.GraineCalls.callSetupProblemssaid no push credentials were uploaded "for this agent". Push credentials belong to the workspace, one set per app, and Graine rings a phone only with the set saved for its own bundle ID or package name (Each app rings with its own credentials). The sentence now says the agent's workspace has no push credentials for this app's bundle ID (iOS) or package name (Android). The code, and the fix it names (Workspace settings → In-app calling), are unchanged.
Hosted ringing — an app is rung only with its own push credentials
4 November 2026. A server change: no SDK version changes and no app needs an update.
- An app hears whether Graine rings it. Push credentials belong to the workspace, one set per app, and a phone
rings only with the set saved for its own app (its bundle ID or package name). An app whose own credentials
aren't saved, such as a second app that uses an agent of the same workspace, was told calls ring there because
the workspace held another app's, and offered booking for calls that could never ring. It is now told they
don't: it registers no push token,
bookingAvailableis false withbookingUnavailableReasonrings_not_set_up, and a booking it sends anyway is refused withscheduling_off(reasonrings_not_set_up). - Who is affected. Apps on SDK 0.14.4 or later, which say which app they are. Older SDKs and web pages are answered for the workspace, as before. An app whose credentials are saved rings exactly as it did. See Each app rings with its own credentials.
Website loader 1.6.1 — a visitor's first conversation waits for the identity token your page is still fetching
4 November 2026. Only the website loader changes: iOS and Android stay at 0.14.8, Kotlin Multiplatform at 0.14.9, and
Flutter and React Native at 0.14.4 (React Native 0.47.3, calls 0.8.3). The loader is served, not installed, so every
website runs 1.6.1 now (GraineAgent.version). Live, no upgrade. Asks nothing of you: a page that sets
identityTokenRefresher gets it as it is.
- Requests wait for your refresher's token. A page that identifies the visitor before their identity token arrives,
and sets
identityTokenRefresher, could have that visitor's requests answered as unverified by an agent with Verified users on. Now the loader asks your refresher first — beforememories(),forgetMemory(),newConversation(),scheduledCalls(),scheduleCall(),cancelScheduledCall(),startWorkflow()and a headless page's past sessions — and waits up to 10 seconds for the token, so the request goes out verified. No token in 10 seconds: it goes without one, as before, and the token is used from the moment it arrives. - The widget's first conversation goes with the token. A conversation in the widget can't be held back for long, so
the loader asks your refresher as soon as the visitor shows interest in the launcher — or as soon as the widget
loads, when your page opens it (
open()) or loads it straight away (preload, headless) — and the conversation waits up to one more second for it when it starts. A refresher that answers by then has the first chat or call verified instead of anonymous; a slower one's token is used from the next. - Each visitor gets their own token. The refresher's "at most once every 30 seconds" now counts per visitor: when another visitor signs in, their token is asked for at once, and a token still on its way for the last visitor never reaches the next. A refresh the loader has already waited for in vain is not waited for again.
- Nothing changes when you pass the token to
identify()orsetIdentityToken(), when no refresher is set, or when Verified users is off. SeeGraineAgent.identityTokenRefresher.
Website loader 1.6.0 — what the dashboard sets reaches every website, almost nothing loads until the agent is used, and a conversation follows the visitor
3 November 2026. Only the website loader changes: iOS and Android stay at 0.14.8, Kotlin Multiplatform at 0.14.9, and
Flutter and React Native at 0.14.4 (React Native 0.47.3, calls 0.8.3). The loader is served, not installed, so every
website runs 1.6.0 now (GraineAgent.version). One change can ask something of you: the first.
modechooses among what the agent allows (changed). The agent's What visitors get and Opens on now reach the website.load({ mode })can narrow what is offered, never add what the dashboard turned off: a chat-only agent loaded withmode: "voice"shows the chat, and the console says why;voice.start()andchat.*refuse what the agent does not offer here. If your page relied onmodeto show a call or a chat the agent's settings leave out, change the agent's settings instead. New:load({ start })picks which one opens first where both are offered. SeeGraineAgent.load.- The widget's frame loads on interest. A visible widget's frame is now made when the visitor shows interest — a
pointer over the launcher, the keyboard on it, a tap — or when your page calls
open()oropenBooking(), not as the page loads. Everything your page sets before then (identify(),setVariables(),setScreen(),pin(), its actions and the identity token) reaches it when it loads. A page that needs the frame at once passesload({ preload: true })(data-preloadon the tag,preloadon<graine-agent>); a headlessload()has it at once, as before. - One tag.
<script async src="…/embed/v1.js" data-agent-id="…" data-publishable-key="…">loads the agent by itself: any text option ofload()as adata-attribute,data-variablesas JSON, anddata-preload. It never holds the page up and may sit in the<head>. The two-tag install is unchanged. See Install. - A smaller, cached loader. About 24 KB compressed, down from 54 KB. Browsers keep it for five minutes, so a fix still reaches every site within minutes. The agent's settings are asked for once (the widget takes them from the loader), with no preflight round trip.
- A conversation follows the visitor across pages. On the next page of the same tab, within 30 minutes of the last
message, the chat shows the conversation again and the agent carries on from it, with no new welcome, identified or
not; nothing connects until the visitor writes. The loader keeps only an ID for the tab's conversation in
sessionStorage, never a word of it. Nothing to change on your site. See When a visitor moves to another page. - Phones and the keyboard. On a page under 640 px wide, a compact chat opens as a sheet and stays above the keyboard. Opened from the keyboard, the panel takes focus, and Escape inside it closes it and puts focus back on the launcher. In the chat, Enter sends and Shift+Enter starts a new line.
- The launcher says what is going on. While the panel is closed it shows a dot for a reply that came meanwhile or a call still going, and its label says so; while a call is going, the browser asks before the visitor leaves the page. A voice-only agent's default label is Talk to us.
- Nothing takes the widget down. A component sent with data it cannot draw shows the agent's sentence for it, and the rest of the conversation carries on.
iOS and Android 0.14.8, Kotlin Multiplatform 0.14.9 — components made with Start blank drawn by the SDKs' own views, and their blocks for your own design
1 November 2026. Flutter, React Native and the website loader stay at 0.14.4 (React Native 0.47.3, calls 0.8.3, loader
1.5.0). On iOS, update GraineInAppRTC beside GraineInApp (no change of its own). Kotlin Multiplatform 0.14.9 builds
on ai.graine:inapp-android 0.14.8 and, on iOS, GraineInApp 0.14.8. Asks nothing of you: an app that shows the SDK's
own views gets it by updating.
- Components made with Start blank are drawn. A component built from blocks on the dashboard's Web components
page — headings, text, pictures, dividers and buttons — showed only the agent's fallback sentence in the SDKs' own
views on a phone.
GraineWidgetView, the voice bar and the chat on iOS and Android, and so Kotlin Multiplatform's screens on both, now draw it from its blocks, in order and in the component's look: its accent and corners, its density, its button style (filled, outline or the whole row) and its picture size, in the phone's own shapes. A picture is as wide as the component and cropped to its height, with a neutral fill while it loads. A button answers the agent as the dashboard's preview does —{"action": <its words>}, and its words as the user's turn — after the component's When they tap, like any other tap. One with no block to draw still shows the fallback sentence. - The blocks, for a design of your own, on iOS and Android.
GraineWidget.blockslists a Start blank component's blocks in order, each aGraineWidgetBlockwith a uniqueid, atype(GraineWidgetBlockType) and only the field its type uses:contentfor a heading, text or button,imageUrlfor a picture, as a full URL. It is empty for any other kind. A block of a type this version does not know, or with nothing to draw, is left out, as on every Graine SDK; Kotlin Multiplatform'sGraineComponent.blocks(0.14.8) is now held to the same rule. SeeGraineWidgetBlockon iOS andGraineWidgetBlockon Android.
Kotlin Multiplatform 0.14.8 — components drawn in your own design: their look, their content blocks and what a tap does; and the wait for the agent's reply
29 October 2026. Only the Kotlin Multiplatform library changes: iOS and Android stay at 0.14.7, and Flutter, React Native
and the website loader at 0.14.4 (React Native 0.47.3, calls 0.8.3, loader 1.5.0). Kotlin Multiplatform builds on
ai.graine:inapp-android 0.14.7 and, on iOS, GraineInApp 0.14.7, unchanged. Asks nothing of you: an app that draws
its own components gets all of it by updating, and can now honour everything the Web components page sets.
- Each component carries the look the dashboard gave it.
GraineComponent.styleis the component's Look & feel, resolved exactly as the SDK's own views resolve it: the preset (Gallery, Cards, List, Spotlight or Compact), the picture size, spacing, button and fact chips, one or two columns, where it goes on screen (the screen, a sheet or a card — by its kind unless you chose), the design grammar, and whether the user picks one item or several, and how many. Each is a typed value whosewireis the dashboard's word. A component with no look of its own is Cards in the place its kind takes; a value this version does not know is left out, never guessed. Its corner radius and accent are set only when the component sets them, so it otherwise takes your own design. SeeGraineComponentStyle. - What a tap does, as When they tap set it.
GraineComponent.onSelectsays whether a tap opens a screen (through the oneopen_screenhandler your app registers) or runs one of your actions, with its arguments and the sentence the agent hears; it isnullwhen a tap only tells the agent.tapFor(item, summary)fills it for the item tapped —{id},{title},{summary}and the rest — and returns the action to run, its arguments and the sentence: run it in your app first, then answer withGraine.chat.answerWidget, exactly as the SDK's own views do. SeeGraineComponent.tapFor. - Content blocks for components built from scratch. A component made with Start blank carries its headings, text,
images, dividers and buttons as
GraineComponent.blocks, in order, each with a uniqueid; a block of a type this version does not know, or with nothing to draw, is left out. SeeGraineComponentBlock. - Pictures that load. A picture the dashboard gave as a path on its own site (
/samples/car.png) — in a component's data or one of its blocks — now arrives as a full URL on yourbaseUrl, as the SDK's own views draw it. - The agent is answering — say so.
Graine.componentAwaitingReply(and each agent's) istruefrom the moment your app answers a component withGraine.chat.answerWidgetand a summary until the agent starts its reply — its first words in the chat, its voice or a caption on a call, or its next component — so your UI can show a “thinking” mark beside the choice instead of a tap that looks unheard. It goes back tofalsewhen the conversation ends, when the component is closed withdismissWidget, and after 12 seconds with no reply, as the SDK's own views stop waiting then. SeeGraineAgent.component.
iOS, Android and Kotlin Multiplatform 0.14.7 — the next call is prepared with the user's token
28 October 2026. Flutter, React Native and the website loader stay at 0.14.4 (React Native 0.47.3, calls 0.8.3, loader
1.5.0). On iOS, update GraineInAppRTC beside GraineInApp (no change of its own). Kotlin Multiplatform builds on
ai.graine:inapp-android 0.14.7 and, on iOS, GraineInApp 0.14.7. Asks nothing of you.
- The call's warm-up waits for the token too. The SDK gets the next call ready in the background, so a tap connects at once. While your app's identity token is still on its way (the wait 0.14.6 added for conversations), that preparation now waits for it as well, without holding anything up, and goes once the token arrives — or once, as it is, when your refresher has none or 10 seconds pass. A launch no longer shows in the Security log as an anonymous user. Answering a call that rings never waits.
- On Android, nothing blocks while it waits, and a call prepared ahead of the tap is kept and dialled when the token arrives, instead of being prepared again.
iOS, Android and Kotlin Multiplatform 0.14.6 — a user's first conversation waits for the token your app is still fetching
28 October 2026. Flutter, React Native and the website loader stay at 0.14.4 (React Native 0.47.3, calls 0.8.3, loader
1.5.0). On iOS, update GraineInAppRTC beside GraineInApp (no change of its own). Kotlin Multiplatform builds on
ai.graine:inapp-android 0.14.6 and, on iOS, GraineInApp 0.14.6. Asks nothing of you: an app that sets
identityTokenRefresher gets it by updating.
- The first conversation waits for your refresher's token. An app that identifies the user before their identity
token arrives, and sets
identityTokenRefresher, could have that user's first conversation answered anonymously by an agent with Verified users on. Now the SDK asks your refresher before a conversation starts — the chat, the voice bar, a call your app places — and before a request for the user (memories, past sessions, a new conversation, booked calls,startWorkflow), and waits up to 10 seconds for the token, so the conversation goes out verified. No token in 10 seconds: it goes without one, as before, and the token is used from the moment it arrives. Answering a call that rings never waits. Nothing changes when you pass the token toidentifyorsetIdentityToken, when no refresher is set, or when Verified users is off. See Secure your agent. - Each user gets their own token. The refresher's "at most once every 30 seconds" now counts per user: when another user signs in, their first conversation asks at once, and a token still on its way for the last user never reaches the next one.
- Users identified without a token can still be rung. Registering the device for the calls Graine rings waits for the token the same way, out of sight; a registration that went without it is made again, once, when the token for the same user arrives — never for a user who has signed out since.
The Security log, and guides to secure your agent — live, no upgrade
28 October 2026. Asks nothing of you.
- Security log. Every check an agent makes is a row: when, which check (Only my apps, Verified app, Verified user, Server token, Bot check, Key or Allowed domains), the result (Allowed, Verified, Refused, Anonymous, or Would be refused while a switch is report-only), why in plain English, the app ID and platform, a one-way install reference, the country, and which part of the agent — chat, call, memory or booking. Kept 30 days, with filters by time, check, result and app ID, and a CSV download. Embed & Widgets → Install → Your app and your users → What we've seen → Security log, and for every agent under Security → Security log; Admins only. Never logged: IP addresses, user IDs, names, phone numbers, emails, tokens, proofs or keys. See Security log.
- Its own page, with pages. Security → Security log, beside the Overview tab, holds every agent's log with a filter by agent, its filters in the page's address so a view can be shared; each agent's own log opens it filtered to that agent. 25, 50 or 100 rows a page, 1–50 of N (counted up to 10,000), Previous and Next.
- Tidier grouping. A refused request is one row: the check that refused it. A pass, or a report-only or anonymous result, is one row per install and check every 10 minutes, whichever part of the agent it came from — report-only and anonymous ones with a count (×N). An app's start-up request shows its install once the app has proven itself, and a request that doesn't name its app reads The request didn't say which app it is (an SDK older than 0.14.4, or not an app at all). See how rows are grouped.
- Changes to the security settings are logged too: which member changed what, and their role — never a secret or a key.
- Each row in full. Click a row (or press Enter on its time) for what it means and what to do, every detail with a
plain label — the switch at the time, how the app or the person was proven, a server token's scopes, the app version
when the app sends it — how the request was decided check by check, and shortcuts to the same install, app or check.
A change shows before → after. Every answer carries an
X-Graine-Request-Idheader, the panel's Request ID, to match a report to its row. See one row in full. - Find a request's rows by its ID. Paste the request ID from an error into the log's search box to see that request's rows, across all 30 days; a row's panel has Show every row of this request. See the request ID.
- New guides. Which way to prove your users (server tokens for sensitive data such as health or money, your sign-in's ID token, or a signed user ID), what a JWKS URL is and how to check yours, every agent your app uses needs its own lock, and test it end to end.
iOS and Kotlin Multiplatform 0.14.5 — the same words for the chat and the call on every SDK, and events that stay under 8 KB
27 October 2026. Android, Flutter, React Native and the website loader stay at 0.14.4 (React Native 0.47.3, calls 0.8.3,
loader 1.5.0). On iOS, update GraineInAppRTC beside GraineInApp (no change of its own). Kotlin Multiplatform's Android
side builds on ai.graine:inapp-android 0.14.4, unchanged.
- The chat's status in the words every SDK uses. On iOS (
GraineChat.status) and Kotlin Multiplatform (Graine.chat.status) the chat now saysidle,connecting,connected,reconnecting,offline,failedordisconnected— the words Flutter and React Native already used.connectedis whatonlinewas:.online/GraineChatStatus.ONLINEstill compile, as deprecated aliases. A chat that gave up saysfailed(it saidoffline), one Graine ended saysdisconnected(iOS saidoffline), andofflinenow means a message waiting with nothing connecting it. Check your code: aswitchorwhenover every case without adefaultorelseneeds the new ones, and code that readofflineas "gave up" should readfailed. See the iOS and Kotlin Multiplatform references. - A native call's phase, in one word.
GraineCalls.phase(iOS) andGraine.calls.phase(Kotlin Multiplatform) sayidle,ringing,calling,connecting,active,reconnecting,endedormissed— thephaseFlutter and React Native already have, mapped the same way. SeeGraineCallPhase(iOS) andGraineCallPhase(Kotlin Multiplatform). reportEventreaches a native call (iOS, and Kotlin Multiplatform on iPhone). An event your app reports while the user is on a native call with the agent is told to that call's agent, so it can speak up there too; before, it was dropped. On Android a native call does not hear it yet.reportEventis held to 8 KB (iOS, Kotlin Multiplatform). An event bigger than 8 KB as JSON is never sent — sent, it would have closed the conversation — and the SDK says why:GraineListener.onErroron iOS, aGraineEvent.ErroronGraine.eventsin Kotlin Multiplatform. Send an ID the agent can look up, not the whole record.
iOS, Android, Flutter and Kotlin Multiplatform 0.14.4 · React Native 0.47.3 and calls 0.8.3 · website loader 1.5.0 — calls that start with the agent's voice, booking that follows the settings, and the agent speaking first
27 October 2026. Nothing to change in your app; on iOS, update
GraineInAppRTC beside GraineInApp.
- Start a workflow from your app.
startWorkflow(workflow, data)starts one of your outreach workflows for the user your app signed in — a welcome call after sign-up, a follow-up after an order — with no server of yours; the workflow needs Your app can start this on. It answers{ runId, workflowId, alreadyRunning }: the user's run already under way comes back, never a second, and a retry with the same idempotency key never starts two (the SDK makes one per call, and asks once more when a request got no answer). The run is always for the signed-in user and never sends to an address the app gives. New error codes on every SDK:not_allowed,not_verified. React Nativeclient.startWorkflow, websitesGraineAgent.startWorkflow; see each platform's reference (React Native, iOS, Android, Flutter, Kotlin Multiplatform, Website). - Your app and your users. Every request now says which app it comes from, so an agent can accept only
your apps;
prepareAppVerification()proves your genuine build with App Attest (iPhone) or Play Integrity (Android, addcom.google.android.play:integrity);identify(traits, identityToken)withsetIdentityTokenand anidentityTokenRefresherproves who the user is. Debug builds send a debug token (debugTokenMode,debugToken) so they keep working once you enforce. New error codesapp_not_allowed,app_not_verifiedandidentity_unverified. Nothing changes until you turn it on in the dashboard. See Secure your agent. - The call starts when the user picks up, with the agent's first word. Nothing reaches the agent, and nothing is billed, until the user answers. Then the greeting plays from its start as soon as the phone's audio does: it is ready before the call connects, so there is no pause while it is prepared, and it waits a moment longer only while the phone's audio is still starting — after a redial within a second of the last call, for example.
- A greeting is never cut on the standard connection (iOS). When a headset or car kit connects, or the system takes the call's audio away for a moment, what the agent was saying carries on from where it stopped.
- Talk, then call (Android). Placing a call while a voice conversation in the assistant bar is still ending waits for it to let go of the audio, so the call starts with sound. A call placed again right after the last is never ended by the one before it.
- A ring lasts as long as Graine rings it (Android): the ring time set under Calls & scheduling, not the app's own.
- Kotlin Multiplatform
startCallnever places a call after it returnednull. - Call History and billing, from this release: a call picked up in the last second of its ring counts as answered; a call that was answered but never connected says so; a ring the user declined is recorded as declined even when the answer arrives late; and one call is one row and one charge, even when it had to switch connection on the way.
- Calls start with the agent's voice on iPhone. A call over WebRTC — most often one answered from a ring on the lock screen, or one placed again right after the last — could connect with nothing heard until the app was reopened. The SDK now starts the phone's call audio again when the system takes it away during a call, and checks that the agent's audio reaches the speaker: when it does not, the audio is started again, and a call that still cannot be heard ends with "The call's audio could not start."
- Calls on Bluetooth headsets and car kits keep their audio when the device connects mid-call (iOS). A call moved to AirPods, a car kit or a wired headset — or to the loudspeaker and back — keeps going while the new route settles, and a call started while a headset is connected starts on the headset, never the loudspeaker.
- The agent waits for the phone's audio. On iOS and Android a call tells Graine once its audio is playing, so the agent's greeting is never spoken before the user can hear it — on calls the user places, rings they answer and redials alike.
- Calls stay connected through weak and changing networks (iOS, Android, and through them Kotlin Multiplatform, Flutter and React Native calls). When a WebRTC call's connection drops — a lift, a tunnel, Wi-Fi to mobile data — or its audio does not start right after it is answered, the SDK reconnects it inside the same call: the conversation carries on and nothing is said twice, and a moment without any network is waited out instead of ending the call. The call moves to the fallback connection only when its own connection truly cannot carry audio.
- Slow and restrictive networks connect. On a slow connection the SDK waits longer before it gives up; a call the network loses on the way is placed again, up to twice, once the phone has a network; and on a network that blocks UDP (some offices and mobile networks) the next try uses only the relays reached over TCP and TLS.
- Kotlin Multiplatform
bookingState.Graine.bookingStateis aStateFlow<GraineBookingState>that follows the agent's settings,identifyandsetMemory: collect it to show your own booking button instead of readingbookingAvailableonce at launch. See the Kotlin Multiplatform reference. reportEventon Kotlin Multiplatform, iOS and Flutter. Something worth speaking up about — the user opened the chat from a reminder — and the agent may speak first, on the live chat or call. React Native and websites already had it.- The right reason while the agent's settings load. Memory, booking, scheduling and past sessions wait for the agent's settings
on every SDK, and report why they could not load (
unavailable,network,invalid_key,origin_refused) instead of "not identified".identity_requirednames the trait the agent identifies users by; 502 and 504 read asunavailable. On websites a failedload()still resolvesfalse(it never rejects); what the page asked meanwhile, and asks after it, fails at once with the session's own reason, and the nextload()starts afresh. - Website
memorySettings.GraineAgent.memorySettings—{ identityField, memoryEnabled }— says which trait identifies visitors and whether the agent remembers them. See Website reference. - Booking says "not yet". While a user is identified and the agent's settings are still to come, booking reads say
not_loadedand the booking sheet never asks them to sign in. - A request is answered only for the user who asked (iOS, Android, Kotlin Multiplatform, Flutter, React Native, websites). Memory, booking, workflow and transcript
requests made for a user who signs out before the agent's settings arrive — another user signing in on the phone — are refused
with
identity_required, never carried out for the next user. - A memory choice is the user's it was made for (iOS, Android, Kotlin Multiplatform). A choice made before the agent's settings arrived, or taken on by an agent configured later, is never filed for the next user on the phone.
- Sign-outs decided before the settings arrive (Flutter) sign out only a user who has left — never one who is back, nor the same user written another way — and still happen when the settings could not load at first.
- NaN and infinity in event data are
null(Android, and Kotlin Multiplatform on Android):reportEventandtrackno longer throw on them. - The same user written another way is the same user (iOS, Android, Flutter). Re-identifying a user spelt another way —
Asha@Example.comthenasha@example.com, a phone with and without its country code — no longer signs them out (which unlinked this phone from their booked calls), starts their chat over or loses their memory choice: users are compared as Graine keys them. - One rule for a refusal's code, on every SDK (
sdk/contract/http-status.json). The code Graine sends wins (trimmed; a blank one is none). Otherwise 400 and 422 areinvalid_request, 401invalid_key, 403origin_refused, 404not_found, 429rate_limited, 502, 503, 504 and the CDN's 520–524unavailable— worth trying again — and any other statusunknown. Before, Android read every 5xx (a 500 or 501 too) as unavailable and a 409 as invalid_request, and no other SDK read 520–524 as the outage they are. - An event keeps its name (every SDK, and calls through the hosted call page — live, no upgrade). A key named
nameordetailinreportEvent's data never replaces the event's own;cooldown_seconds,max_per_sessionandquietsit at the event's level, and everything else travels under itsdata— the same event in the chat and on a call. - Booked calls say why the settings could not load (iOS):
scheduledCalls()andcancelScheduledCall(id:)throw that reason instead of answering for this install alone, as Android does. bookingStatenever keeps a stale answer (Kotlin Multiplatform): a refresh racing a sign-out on another thread can no longer leave itavailable; and on iOS, booking read on another thread as the settings arrive never saysnot_identifiedfor an identified user.- Server tokens —
tokenProvider. Give the SDK a function that returns a short-lived token your server minted for the signed-in user (POST /v2/client-tokens, with your secret API key) and every request carries it; the SDK asks again shortly before each token expires, when a call rings, whenidentify()names another user, and when Graine refuses one — and a token that expires never ends a chat or call in progress. Needed once the agent's Require server tokens is enforced; without a provider nothing changes. New codes:token_required,token_invalid,token_expired,token_scope, with the server'sreason. See Server tokens. key_revoked. An app or page still using a publishable key the agent's Admin refused says so with its own code, and never retries. See Rotating the publishable key.- The website bot check (loader 1.5.0). When an agent turns it on, the loader passes an invisible Cloudflare Turnstile or reCAPTCHA Enterprise check before a conversation starts — your page needs nothing beyond the loader. See Website bot check.
- What a call proves reaches React Native and Flutter (React Native with the calls package, Flutter). Graine's verdicts on a
call's own requests — the app proven, the customer proven — now reach the client, so
useGraineVerification()and Flutter'sverificationshow what a call proved: only ever more proven, never less, and only for the customer identified now.
The voice engine keeps WebRTC calls connected — live, no upgrade
26 October 2026. Asks nothing of you.
- A lost connection is reconnected inside the call. When a WebRTC call's audio path drops mid-call — a lift, a
tunnel, Wi-Fi to mobile data — the engine reconnects it within the same call: the conversation carries on and nothing
is said twice.
reconnectingis true meanwhile, soGraineVoiceBarshows your reconnecting words instead of Listening, and a moment without any network is waited out rather than moving the call. The call moves to the standard path only when its own connection cannot come back. See On a weak network. - A call answered without audio is reconnected before it moves. The engine reconnects it first — at once when nothing at all is getting through — and moves the call to the standard path only if that fails: after 8 seconds on a good network as before, longer on a slow one, never later than 30 seconds. See While it connects.
- Slow and restrictive networks connect. Waits grow on a slow link and a connection that is still setting up is not cut; a call the network loses on the way is placed again, up to twice, never while the device has no network; and on a network that blocks UDP the next try uses only the relays reached over TCP and TLS, port 443 included. See Mobile networks.
- The agent's greeting waits for the customer's audio. A WebRTC call on your website or in your app's voice bar now tells Graine once its audio is really playing, as the native call engines do, so the greeting is not spoken into a connection that cannot be heard yet — on a slow or lossy network, or one that needed reconnecting first.
The voice engine on weak networks — live, no upgrade
26 October 2026. Asks nothing of you.
- Connected means the customer can hear the agent. On a slow or lossy network a WebRTC call's audio can start a
few seconds after the call is answered. The engine now reports the call as connecting until its audio can flow, so
GraineVoiceBarno longer says Listening over a silence. A call whose audio never starts still moves to the standard path within 8 seconds. See While it connects. - A short network drop no longer restarts the call. When the device loses its network for a few seconds mid-call,
the call carries on as soon as the network returns, and the agent sees the screen again once its connection is back.
Before, the conversation could restart on the standard path after five seconds — or end, if the network was still
gone at that moment. See When
rtccannot be used. - Connection quality is the last few seconds.
qualityis judged on what the call is getting now: poor while no audio arrives, back to excellent when it recovers — no longer excellent through an outage, or poor for minutes after a brief one. See On a weak network.
iOS, Android, Flutter and Kotlin Multiplatform 0.14.3 · React Native 0.47.2 and calls 0.8.2 · website loader 1.4.2 — past sessions, and a chat that closes with its screen
22 October 2026. One behaviour to know: a chat session now closes 15 seconds after its screen closes (chatCloseDelaySeconds sets it).
- Past sessions, like Execution History.
chat.pastSessions— the signed-in user's earlier sessions with the agent, from Call History, newest first and paged: id (the one Execution History shows), start and end, duration, chat / voice / call, direction, how it ended, message count and a preview — withrefreshPastSessions(),loadMorePastSessions()andsessionTranscript(id)for one session's full chat history. Only the user's own sessions, only on a device bound to them. See Chat → Past sessions. - A chat session closes with its screen. When no window shows the chat, its session closes
chatCloseDelaySecondslater (15 by default, 0 = at once, at most 300) instead of staying connected for 5 idle minutes, so a chat nobody is looking at is never an open, billed session. Opening the chat again continues the conversation from its history. On websites the chat now connects when the visitor first opens it. - Call History words a chat as a chat: Chat closed, Closed after inactivity or Agent ended the chat.
iOS, Android, Flutter and Kotlin Multiplatform 0.14.1 · React Native 0.47.1 and calls 0.8.1 — booking within calling hours, Talk levels, the call's transcript
21 October 2026. Additions only: nothing to change in your app.
- Booking follows the agent's calling hours.
bookingSlots(day)and the booking sheet offer only times inside the agent's calling window, in the user's time zone; the scheduling rules carry the window (callingWindow). - Talk levels everywhere. The agent's and the user's voice levels (0–1) move on Android, Flutter and React Native as they already did on iOS, and on websites over WebRTC, for a visualiser of your own.
- The call's transcript. Android
GraineCalls.captionsand Kotlin MultiplatformGraineCallInfo.captions: the call's recent lines. - Kotlin Multiplatform
Graine.chat.failed: true once the chat's retries are spent, until the user retries.
iOS, Android, Flutter and Kotlin Multiplatform 0.14.0 · React Native 0.47.0 and calls 0.8.0 · website loader 1.4.0 — every screen yours to draw, a chat that continues, steadier calls
21 October 2026. Two things to know before you update: agent calls now start on the loudspeaker (speakerByDefault = false
keeps the earpiece), and code that matches every chat status or chat row kind needs the new cases.
-
Every screen, your own design. Every state the SDK's own screens read, and every action their controls perform, is now public on every platform, so your app can draw its own chat, Talk bar, call screen, booking picker and agent components with no Graine-drawn view. The SDK keeps running the connection, the audio, the calls and the agent:
- Chat: the messages, the connection status (connecting, connected, reconnecting, offline, failed, disconnected),
typing, suggested replies, the end-of-chat rating, where a new conversation begins, and
shown(visible), which a chat screen of your own calls so the conversation lives as the SDK's own window's does. - Talk: the status (connecting, listening, thinking, speaking, reconnecting, muted, failed, ended), live captions, and the transport with the reason it fell back.
- Calls, both directions: the status, mute and speaker, the duration, captions, the agent's voice level, what ended the call, and a connection report, live and at the end: setup, login, answer, first-words and tap-to-voice times, packet loss, jitter, round trip, a good, fair or poor rating, and recoveries. These are the facts of the Connection card in Execution Logs.
- Booking: whether the user can book and, when not, why (
not_loaded,rings_not_set_up,scheduling_off,not_identified), the scheduling rules, the times the booking sheet itself offers for a day, and the user's booked calls. - Components the agent draws: the kind, its data, a plain-text fallback and a loading state; your controls send the answer or dismiss it.
Each platform's names are on its reference page: iOS, Android, Kotlin Multiplatform, Flutter and React Native.
- Chat: the messages, the connection status (connecting, connected, reconnecting, offline, failed, disconnected),
typing, suggested replies, the end-of-chat rating, where a new conversation begins, and
-
Websites draw their own UI too.
GraineAgent.load({ headless: true })draws nothing and hands your page the same chat, Talk and component states (state(),on("state")), withchat,voiceandcomponentactions. See Website → Draw your own UI. -
Closing the chat no longer ends the conversation. It stays connected until it has been idle for 5 minutes — no message either way — instead of closing 15 seconds after the chat leaves the screen. Opening the chat again within that time continues exactly where it was: same conversation, no new welcome. See Chat → How long a conversation stays open.
-
"Disconnected" when Graine ends an idle conversation. The chat no longer reconnects by itself into a new conversation the customer did not ask for: the messages stay, the header says Disconnected, and opening the chat or sending a message connects again. A dropped network connection still reconnects by itself, as before.
-
No more stacked welcomes. When the chat connects again and the conversation cannot continue (an anonymous customer, memory off, or earlier messages that could not be read in time), the window no longer adds the agent's welcome under the earlier messages again. An opening the customer never answered is replaced by the new one; after a conversation the customer took part in, a quiet line — Disconnected · new conversation — shows where the new one begins. A conversation that continues opens as before, with no line. See Chat → When the conversation cannot continue.
-
Calls start on the loudspeaker unless a headset is connected.
setSpeaker(false)moves a call to the earpiece as before, the speaker state is the route the phone really uses, andspeakerByDefault = falsestarts calls on the earpiece. -
Calls connect sooner on a slow network. A call no longer waits on a background check before it dials, its setup request takes Graine's web route after 1.5 seconds, and the 12-second connect budget starts once setup has answered.
-
Why a call ended reaches your app (
endCause:local_end,remote_bye,media_drop,audio_watch,failed), and every call's end is a row in Execution Logs. -
A chat belongs to the user it was opened for. Identifying the user after the chat opened reconnects it as that user, and their conversation continues; signing out or switching user clears the previous user's lines, keeping any not yet sent.
-
Chat sessions, each with its id:
sessionId,sessions(id, start and end, chat or voice, message count, current) andsessionon every message — the id Execution Logs shows, for a list of past conversations in your own design. -
On iPhone, a call's audio always starts. The SDK releases the phone's audio before every call, starts it itself when iOS has not, and never ends a call that is playing the agent. On Android, the call's audio is set before the agent speaks, and a working call is never ended by the app's call service or mistaken for a phone call.
-
Your own call screen in Kotlin Multiplatform:
GraineCallsConfig.androidCallActivityon Android. -
The web component draws your own UI too:
<graine-agent headless>, with the same state and actions. -
Every SDK: iOS
GraineInAppandGraineInAppRTC, Androidai.graine:inapp-androidandai.graine:inapp-android-rtc, Kotlin Multiplatformai.graine:inapp-kmpandai.graine:inapp-kmp-compose, Fluttergraine_inapp, React Native@graineai/inapp-react-native.
iOS, Android, Flutter and Kotlin Multiplatform 0.13.1 · React Native calls 0.7.1 — a WebRTC call answered on its first dial
20 October 2026. Asks nothing of you.
- A WebRTC call no longer redials while the agent is picking up. On a slow connection the half-second wait for the agent to answer could start before the server had the call, so a call about to be answered was cancelled and dialled again: a longer Connecting…, and at times the agent's greeting captioned twice and an extra, empty call in the call history. The wait now starts once the server confirms it has the call, and a dial that must be made again is always ended on the server first, so each call is answered by one agent.
- The voice bar got the same fix on 20 October — live, no upgrade — and its status now reads Connecting…, never Reconnecting…, until a call has connected.
- Every SDK: iOS
GraineInAppandGraineInAppRTC, Androidai.graine:inapp-androidandai.graine:inapp-android-rtc, Kotlin Multiplatformai.graine:inapp-kmpandai.graine:inapp-kmp-compose, Fluttergraine_inapp, React Native@graineai/inapp-react-native-calls.
Prompt variables while your website is open — live, no upgrade
20 October 2026, website loader 1.3.0. Asks nothing of you: the script at /embed/v1.js is always the current
loader, and a page that never calls the new functions works exactly as before.
GraineAgent.setVariables(values), as every app SDK has it: merged into what is set,nullunder a name removes it (load()'s value for that name, else the agent's default, applies again), andsetVariables(null)clears them all. Values become text and are masked in the browser; they reach the next chat or voice conversation. They belong to the page: they stay whenload()puts another agent on it. Safe beforeload().GraineAgent.currentVariables()returns the set the next conversation carries — the agent's defaults,load()'svariables,setVariables(), then the traits — masked.- A sign-out clears the visitor's traits from the widget too. After
identify(null), a voice conversation started on the same page no longer carries the previous visitor's traits. - See Send your user's details to the agent and the Website SDK reference.
iOS, Android, Flutter and Kotlin Multiplatform 0.13.0 · React Native 0.46.0 — a chat window you design in Garage
20 October 2026. Asks nothing of you: a design saved before looks exactly as it did.
- Design the chat window in Garage, with no app release. Ten chat styles — Classic, Soft, Bold, Glass, Minimal, Crisp, Cozy, Calm, Compact and Lively — and every piece on its own: the header (surface, accent, gradient or none, centred or not, a status line, a call button), the bubbles (rounded, squircle, tailed or flat, their corners, width, grouping, shadow and colours or gradients per side), times and ticks, the wallpaper (solid, gradient or a pattern), chips, cards, the composer (bar, floating or pill, a microphone, a paperclip), the typing indicator, density, the welcome (centred, card, hero or minimal) and the rating. Every SDK's chat window and the website's chat draw it.
- Replies your assistant suggests appear as chips; a tap is the customer's own message.
- "How was this chat?" When your assistant ends a chat, the chat can ask — thumbs, stars or faces. The answer is
kept with the conversation, and your app hears it (
chat.onRated, React Nativechat_rated). - Pin what the chat is about:
chat.pin(…)(React NativesetChatPin, the websiteGraineAgent.pin) shows a title, a line, a picture and a badge as a bar or a card. Never sent to your assistant. - Attachments: set
chat.onAttach(React NativeonChatAttach) and the paperclip shows where the design draws one. - A smoother chat: replies revealed as they are written, light formatting drawn, a long press to copy, messages that wait for the connection with Tap to retry when one could not be sent, a calm reconnecting banner, a pill to the latest message, and screen readers, text size, reduced motion and right-to-left throughout.
- Always readable. Every word keeps at least 4.5:1 contrast on its background, gradients and glass included.
- For apps that draw their own chat: the whole look is in the theme tokens (
chat), with the suggested replies, the end of the chat and its rating on each SDK's chat object. - Every SDK: iOS
GraineInAppandGraineInAppRTC, Androidai.graine:inapp-androidandai.graine:inapp-android-rtc, Kotlin Multiplatformai.graine:inapp-kmpandai.graine:inapp-kmp-compose, Fluttergraine_inapp, React Native@graineai/inapp-react-native.
iOS, Android, Flutter and Kotlin Multiplatform 0.12.0 · React Native 0.45.0 and calls 0.7.0 — a call screen you design in Garage
20 October 2026. Asks nothing of you: a design saved before looks exactly as it did.
- Design the call screen in Garage, with no app release. Ten ready looks, or your own: a solid, linear, radial, mesh or blurred-avatar background with up to five colours and gentle motion; the avatar's ring (pulse, ripple or glow); glass, solid or outline buttons with their shapes and colours; a voice visualiser that follows the agent; caption styles; type; and a look of its own for each moment of a call — ringing, calling, connecting, on the call, reconnecting, ended, missed and scheduled. Every SDK's native call screen draws it.
- Always readable. Words and icons keep at least 4.5:1 contrast on any background, gradients and glass included.
- "Reconnecting…" when the connection drops for a moment, and the visualiser follows the agent's real voice on every
connection. For an app that draws its own call screen, each SDK's call state carries
reconnectingand the agent's voice level. - Android: a call set up with
prepareCall()keeps the microphone off until the tap. With 0.11.0, the phone's microphone indicator could come on while a prepared call waited, though nothing was dialled or sent. iOS was not affected. Nothing to change in your app. - For apps that draw their own call screen: the whole look is in the theme tokens (
call), on every SDK. - Every SDK: iOS
GraineInAppandGraineInAppRTC, Androidai.graine:inapp-androidandai.graine:inapp-android-rtc, Kotlin Multiplatformai.graine:inapp-kmpandai.graine:inapp-kmp-compose, Fluttergraine_inapp, React Native@graineai/inapp-react-nativeand@graineai/inapp-react-native-calls.
iOS, Android, Flutter and Kotlin Multiplatform 0.11.0 · React Native calls 0.6.0 — calls connect faster, and each call reports how it connected
19 October 2026. Asks nothing of you: prepareCall() is an optional addition.
- Calls connect faster. The SDK now sets each WebRTC call up with Graine's call servers directly, with a short-lived permission it fetches ahead in the background, instead of an extra hop through Graine's web service. A call that can't use it connects exactly as before. Nothing to change in your app.
- New, optional:
prepareCall()— the next call set up before the tap. Call it on a screen with a call button, each time the screen appears, with the same reason and variables you will pass tostartCall; the tap then only dials. Nothing is dialled, recorded or billed before the tap — the microphone stays off until the user taps Call (on Android from 0.12.0). The prepared call is let go when the app goes to the background, when the signed-in user, their memory choice or their time zone changes, after 4 minutes, or when a call rings. iOSGraineCalls.shared.prepareCall(reason:variables:agent:), AndroidGraineCalls.prepareCall(reason, variables, agent), Kotlin MultiplatformGraine.calls.prepareCall(reason, variables, agent), FlutterGraineCalls.instance.prepareCall(reason:, variables:, agent:), React Native callsprepareCall({ agent, reason, variables }). - Every call reports how it connected, in Execution Logs: WebRTC or the standard connection (and why), whether it was set up ahead, the time from the tap to the agent's first words, and the call's audio quality. Timings only — nothing personal. The web SDK and the voice bar report the same; they update with Graine.
- Every SDK: the native engines (iOS
GraineInAppandGraineInAppRTC, Androidai.graine:inapp-androidandai.graine:inapp-android-rtc), and so Kotlin Multiplatform, Flutter and the React Native calls package.
iOS, Android, Flutter and Kotlin Multiplatform 0.10.3 · React Native 0.44.3 and calls 0.5.3 — a stalled WebRTC dial is redialled after half a second
18 October 2026. Asks nothing of you. Replaces 0.10.2, withdrawn the same day.
- A stalled WebRTC dial is redialled after half a second, not 3 seconds. A dial the server has acknowledged is answered within a few dozen milliseconds; one still unanswered half a second after that is cancelled and made once more on a fresh connection, and a second stalled dial continues on the standard connection, with
transportFallbackReasonno_answer. 0.10.2's 3 seconds only delayed that fallback. Nothing to change in your app. Every SDK: the native engines (iOSGraineInAppRTC, Androidai.graine:inapp-android), and so Kotlin Multiplatform, Flutter and the React Native calls package. - The same in the browser and in the voice bar. The hosted engine — the web SDK, and the voice bar on every platform — gives an acknowledged dial the same half second. It updates with Graine: nothing to install.
iOS, Android, Flutter and Kotlin Multiplatform 0.10.2 · React Native 0.44.2 and calls 0.5.2 — a stalled WebRTC dial is redialled after 3 s
18 October 2026. Withdrawn the same day — use 0.10.3. React Native 0.44.2 and calls 0.5.2 were never published.
- A WebRTC call the server acknowledges but never answers is dialled again after 3 seconds. The engine used to wait out the whole 12-second WebRTC attempt for such a call — a long Connecting…, then the call continued without WebRTC — because its 15-second answer ceiling could never fire inside that budget. Now, 3 s after the server's first reply with no answer, the stuck dial is cancelled and made once more on a fresh connection (what a second tap did by hand); a second stuck dial continues on the standard connection, with
transportFallbackReasonno_answer. Nothing to change in your app. Every SDK: the native engines (iOSGraineInAppRTC, Androidai.graine:inapp-android), and so Kotlin Multiplatform, Flutter and the React Native calls package. - The same in the browser and in the voice bar. The hosted engine — the web SDK, and the voice bar on every platform — now gives a dial the server has acknowledged 3 seconds to be answered as well, where it waited up to 15 seconds for each of its two dials. It updates with Graine: nothing to install.
iOS, Android, Flutter and Kotlin Multiplatform 0.10.1 · React Native 0.44.1 and calls 0.5.1 — on Android the SDK registers for pushes itself: no messaging service to write
15 October 2026. Asks nothing of you: an app that already has its own messaging service keeps working unchanged.
- Add Firebase to your app and you are done. With Firebase Cloud Messaging in the Android app
(
google-services.json, its Gradle plugin and the messaging library), the SDK's own messaging service — merged from its manifest — rings every call push, even when the app is closed, and receives every new token; at start the SDK also reads the current token, so a device registers for hosted ringing without a line of your code. Firebase stays optional: an app without it builds and runs as before. - React Native: with Expo, name your
google-services.jsoninandroid.googleServicesFileand the calls package's plugin brings Firebase Cloud Messaging (firebaseMessaging: falseleaves it out); in a bare app,graineFirebaseMessaging=trueinandroid/gradle.properties. Flutter:com.google.firebase:firebase-messaginginandroid/app/build.gradle.kts, no Dart. - Already have your own
FirebaseMessagingService(or@react-native-firebase/messaging, orfirebase_messaging)? It outranks the SDK's and receives every message, so keep handing Graine's pushes and tokens over —handlePushandonNewToken/registerPushToken, as before. See In-app calling. no_push_tokennow says what is missing — Firebase, its setup, automatic token creation turned off, or why Firebase gave no token — with the fix.
iOS, Android, Flutter and Kotlin Multiplatform 0.10.0 · React Native 0.44.0 and calls 0.5.0 — the SDK asks for call permissions and tells you what setup is missing
15 October 2026. Asks nothing of you: the new calls are additions.
- Ask for everything a call needs in one call.
requestCallPermissions()asks for the microphone, notifications (Android 13+) and the full-screen ring (Android 14+, opening the right Settings page), andcallPermissionsreads each as granted, denied, blocked or not needed;openCallSettings()opens the page to change one. Ask when it suits your app, for example during onboarding — the SDK never asks by itself at launch. - A setup check.
callSetupProblemslists what is still missing for calls to ring — the iOS microphone sentence or background modes, pushes never enabled, no push token, notifications or the full-screen ring not allowed, push credentials not uploaded for this app — each with the exact fix, and the SDK logs it once at start-up. - React Native: an Expo plugin. Add
@graineai/inapp-react-native-callstopluginsinapp.jsonand it adds the iOS microphone sentence, the background modes and the push entitlement;webrtc: trueturns on WebRTC calls. - See Permissions for the table of calls and setup codes on every SDK.
iOS, Android, Flutter and Kotlin Multiplatform 0.9.0 · React Native 0.43.0 and calls 0.4.0 — calls over WebRTC in both directions, and the same features on every SDK
15 October 2026
Calls over WebRTC
- Opt-in, in both directions. The calls the user places and the calls Graine
rings carry their audio over WebRTC when your app includes the SDK's WebRTC
module: a jitter buffer, loss concealment and the platform's echo canceller keep
the agent's voice steady on a weak or changing network. iOS: the new
GraineInAppRTCpackage (https://github.com/GraineAI/inapp-ios-rtc, orpod 'GraineInAppRTC'), besideGraineInAppat the same version. Android:ai.graine:inapp-android-rtc, at the SDK's version. React Native (calls 0.4.0) and Flutter:ENV["GRAINE_WEBRTC"] = "1"in the Podfile andgraineWebRtc=trueingradle.properties, orGRAINE_WEBRTC=1in the build's environment. Kotlin Multiplatform: each platform's module in the app. Nothing else changes in your code, the SDKs themselves keep no dependencies, and an app that does not add the module never downloads it. See Calls over WebRTC. - Ready before the answer. A call is set up at the tap, or while a ring shows, so it connects as fast as before; a declined or missed ring lets go of what it prepared. On iOS the audio starts only when the system call hands it over, so the agent's greeting is never clipped.
- A call WebRTC cannot carry continues on the WebSocket inside the same call — no second ring, Connecting… a moment longer; WebRTC gets 12 seconds in all. A call the agent cannot take at all (no credit left, the agent switched off or deleted) ends with that reason instead of trying again.
callTransportin each SDK's calls configuration:auto(the default — WebRTC when the module is in the app),websocketorwebrtc.transportandtransportFallbackReasonon the call say which one carried it, and why a call that tried WebRTC did not (mint_failed,register_failed,no_response,no_answer,ice_timeout,error; Android alsodashboardandunavailable). Objective-C: onGraineObjCCalls. See GraineCallTransport.- The dashboard decides last. The agent's In-app call audio setting — WebRTC (recommended) or Standard — keeps every install's calls on the WebSocket from their next start, without a release.
- Size and sound. About 12 MB more installed on an iPhone (6 MB to download),
about 7.4 MB on a 64-bit Android phone; Android's manifest gains
ACCESS_NETWORK_STATE, a normal permission. 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 same features on every SDK
- Empty the chat on screen:
clear()on iOS and Kotlin Multiplatform, as on Android and Flutter;clearChat()in React Native (hook and client, heard aschat_cleared). Only the screen: the conversation goes on, a message still waiting is still sent, and nothing Graine keeps is touched. - A chat says when it is reconnecting, or gave up:
reconnectingon Android and Flutter (Kotlin Multiplatform'sRECONNECTINGfollows it on Android). React Native:useGraineAgent().failedandclient.chatFailed(heard aschat_failed) when the chat's first connect failed or Graine ended or refused the conversation, andretry()(hook and client) connects again, the backoff starting over — a network drop is neverfailed. - The captions switch on iOS, Android, Flutter and Kotlin Multiplatform, as
React Native's:
captionsOn(on by default),setCaptionsOnandclearCaptions(); the SDK's bar hides its caption while it is off, and what was said is kept. Android's voice keeps it incaptions(GraineCaption), and Flutter's switch isGraineCaptionsSwitch. - The rows a chat window draws, for a chat of your own: iOS
GraineChatRows.build(turns)and AndroidChatRows.build(messages), as React Native'schatTurnRows, Flutter'sChatRowsand Kotlin Multiplatform'sGraineChatRows. - Answer a component from your own UI: iOS
answerComponent(widgetId:data:summary:), AndroidanswerWidget(widgetId, data, summary), Kotlin MultiplatformGraine.chat.answerWidget(id, data, summary). React Native'sclient.presentWidget(frame)andpresentPendingWidget(hint)draw a component, or its loader, yourself. - Kotlin Multiplatform:
Graine.theme— the brand's design tokens (GraineThemeTokens) for UI of your own —Graine.currentVariables(), and the chat'supcomingCalls,refreshUpcomingCalls()andcancelUpcomingCall(id). On iOS these needGraineInApp0.9.0 beside the library; with an older one they do nothing (the theme stays null, the calls list empty, the captions on). React Native'sclient.getVariables()is documented as what it is: the merged set the agent will receive. Objective-C gainscurrentVariables,themeJSON, the captions switch,answerWidgetWithId:dataJSON:summary:and the chat's upcoming calls. viewOpened()andviewClosed()on iOS and Android, and Android'svoice.attached, as Flutter's: a voice UI of your own counts as the bar does.- React Native calls: an incoming call says whether Graine rang it —
GraineIncomingCall.hosted, as on iOS, Android, Flutter and Kotlin Multiplatform.
Fixes
- Variables over the ticket's kilobyte are trimmed to fit, not all dropped.
who_calledandcall_reasonride first, and the biggest of the rest are left off one at a time until the rest fit — the same rule on every SDK. Before, a set past 1 KB lost every variable. See Variables. scheduleCall's own refusals say the booking sheet's words, on every SDK and the website: "Booking a call isn't available right now", "Pick a time at least 1 minute from now" (not "1 minutes"), "Pick a time within 14 days".- React Native: the
errorevent fires, and a refused microphone says so:useGraineEventshearserror(message) when the agent or the chat could not start, or the voice launcher reports something to fix (asonErrordoes); a WebRTC voice call the microphone refused reportsmicrophone_denied(it saidconnect_failed) andmic_denied. - React Native: variables ride the ticket alone. A chat or voice connection opened with a ticket carries the variables in it only, as the other SDKs do; a connection opened without one still carries them on its address.
- React Native:
maskStringapplies youraddMaskRulerules (it ran only the built-in ones), andmaskDeepkeeps aDateas its ISO string (it became{}). - Website: an action handler still running is answered
errorat 6 seconds, the SDKs' deadline (it was 5.5).
Action: upgrade — everything is additive. For WebRTC calls, add the module (or the build switch) and ship a store build; nothing else to change. Leave the agent's In-app call audio on WebRTC (recommended) unless a network your users are on blocks WebRTC.
iOS, Android, Flutter and Kotlin Multiplatform 0.8.0 · React Native 0.42.0 and calls 0.3.0 — the SDK's own booking sheet, and calls that never hang on Connecting
15 October 2026
- Booking a call, drawn by the SDK. "Call me at…" with no screen of your
own: the day (chips from today to the furthest day the agent allows), the time
(each platform's own picker, only the times the agent's rules allow), the
phone's time zone, "What's it about? (optional)" and Book call — then
"Booked — … will call you on …" with Done — in your agent's design, a sheet
on a phone and a centred panel on a tablet. It returns the booked call, or
nothing when the user closed it; a refusal stays in the sheet as one line in
plain words. iOS
presentBooking(reason:),.graineBookingSheet,GraineBookingViewandGraineBookingViewController(Objective-CpresentBookingWithReason:completion:); AndroidpresentBooking(activity, reason) { call, error -> }andGraineBookingDialog; FlutterGraineBooking.show(context, reason:)andGraineBookingSheet; React NativeopenBooking({ reason })and<GraineBookingSheet>; Kotlin MultiplatformpresentBooking(reason); the websiteGraineAgent.openBooking({ reason }). See Booking from your app with the SDK's sheet. bookingAvailableon every agent — and Flutter'sbookingBlockerandbookingListenable: Graine rings this phone, the agent lets users book, and the user is identified (on a website, the last two). Show your own Call me button only then.- Book a call in the chat window, under the booked calls in the Upcoming calls card, or on its own where none are booked — where booking is available and the design shows booked calls. A booking refreshes the card.
- A call never sits on Connecting. iOS: everything before the conversation opens shares one 20-second deadline — "The call could not connect within 20 seconds." — and a conversation the phone never gives audio ends with "The call's audio could not start." instead of staying silent. Android: nothing at all arriving for 50 seconds ends a call — "Nothing arrived for 50 seconds: the connection was lost." — a slow network never freezes a call (at most 300 ms of the user's speech waits), and hanging up lets go of the microphone at once. See When a call cannot connect.
- A call starts at once on the agent's settings the SDK already has — this launch's, or the ones kept on the device — and refreshes them behind the call.
- Variables ride the signed ticket only on iOS, Android, Flutter and Kotlin Multiplatform. A chat's and a call's prompt variables are no longer repeated on the address of a connection opened with a ticket; a connection opened without one still carries them. See Variables.
- React Native's calls package 0.3.0 carries the native call engine with these fixes; 0.42.0 adds the booking sheet. The dashboard's In-app call audio setting is ready for WebRTC calls, which arrive in the next release.
- Every public function, property, event, error and type, per platform, grouped by use case and then A to Z: SDK reference. Every use case with its code on every platform: Use cases by SDK.
Action: upgrade — everything is additive. Calls are native code, so the upgrade
needs a store build. To offer booking, turn on Users, from your app and show
your button where bookingAvailable is true.
iOS, Android, Flutter and Kotlin Multiplatform 0.7.0 · React Native 0.41.0 and calls 0.2.0 — users, memory, booked calls and Graine ringing your app
- Who the user is.
identify(traits, { memory })uses the trait your agent's Who is the user? setting names —user_id,email,phoneor a custom field such ascustomer_id, under Agent → Memory → Settings — to recognise the same person across chat, voice and calls. The value is sent only to start a conversation and never becomes a prompt variable.identify(null)signs out. See Identity and users. - Memory. The agent remembers each user across conversations.
setMemory,memories()andforgetMemory()let your app show and erase what is remembered. See Memory. - Several agents in one app. Configure each agent under a name —
GraineInApp.configure(config, name:)on iOS,GraineInApp.init(…, name)on Android and Flutter,Graine.configure(config, name)in Kotlin Multiplatform, aGraineProviderper agent in React Native — and choose one per screen. Who the user is, the install and the memory choice are shared, one conversation runs at a time, andstartCall(…, agent)calls the agent you name; a ring Graine sends answers as the agent that booked it. On a website,load()with another agent switches to it (loader 1.2.0). See Use more than one agent in your app. - Calls the user books, and calls the agent books.
scheduleCall,scheduledCalls,cancelScheduledCallandschedulingRules, with the booking rules checked on the device first. See Scheduled calls. - Graine rings your app. Upload your push credentials in Workspace settings → In-app calling and Graine rings your users for booked calls — CallKit on iPhone, a full-screen call on Android — and reports every outcome. Android apps add two lines to their messaging service. See Graine rings your app.
- Chat on every platform. A native chat window with components on iOS
(
GraineChatView), Android (GraineChatView,GraineChatActivity) and Flutter (GraineChat); React Native's chat now keeps one bubble per reply, reconnects on its own and sends messages typed offline once it is back. - Conversations continue. A user you identify who comes back finds the chat
where they left it — their last messages with that agent, kept for 7 days after
the latest: the recent messages, an earlier call as one "Voice call" row and a
day line where the day changes, and the agent picks up from there instead of
greeting them again.
newConversation()starts a fresh thread (the chat window's header offers it), and a successfulforgetMemory()clears the chat too. Each turn carriesrestoredandchannelfor apps that draw their own chat. See Memory → Conversations continue. - The chat window's size. The Garage's Chat window → Window size —
Compact, Large or Full screen — reaches every bar and the website, and your app
can choose:
GraineConfig(chatSize:)andchat.open(size:)on iOS,GraineConfig(chatSize =)andchat.open(size)on Android,GraineConfig(chatSize:)andGraineChat.open(context, size:)on Flutter,GraineConfig(chatSize =)andGraine.chat.open(size)in Kotlin Multiplatform,chatSize,client.openChat({ size })andGraineChatScreenin React Native, andGraineAgent.open({ mode, size })andload({ chatSize })on a website. See Chat → Window size. - Kotlin Multiplatform.
ai.graine:inapp-kmpis one API in shared code over the iOS and Android SDKs — identity and memory, the screen, actions, the chat (its window size, conversations that continue,newConversation(), andGraineChatRowsfor a chat of your own), the voice bar, calls with each call'sagent, scheduled calls and several agents — andai.graine:inapp-kmp-composeadds the SDK's own chat, call screen and bar for Compose Multiplatform. See Kotlin Multiplatform. - Your design on the chat window and the call screen. The Garage's Chat window and Call screen tabs reach every SDK: header, bubbles, starters, the upcoming-calls card, and every call state (scheduled ring, ringing, connecting, on the call, ended, missed with Call back).
- Screen context behaves the same everywhere: the first change is sent at once,
taps on labelled controls are noticed, and
updateField,removeFieldandclearScreenare on every SDK. - Every function's parameters, results and errors: SDK reference.
Action: upgrade. Flutter renames the native call's status enum to
GraineNativeCallStatus; everything else is additive. To use booked calls and
memory, turn them on in the dashboard and call identify after your own sign-in.
iOS and Android 0.6.2 · Flutter 0.6.3 — a phone call puts the agent on hold
- Android: a call goes on hold when the phone takes its audio — a phone call
ringing, another app's call or player. Nothing the customer says is sent and
nothing the agent says is played until the audio comes back. If the customer
answers the phone call, the agent's call ends (
ended,local), as iOS's End & Accept ends it. - iOS: a call keeps its own variables however many your app has set:
who_called,call_reasonand the push's go first, and your app's are added while they fit. - One shape of id for a call the customer places:
call_and a lowercase UUID, on every platform. - React Native's calls package 0.1.1 carries this engine.
Action: upgrade; nothing in your code changes.
iOS and Android 0.6.1 · Flutter 0.6.2 · React Native 0.40.2 and calls 0.1.1 — calls follow the headset
- Android: calls follow the headset, as a phone call does — a Bluetooth headset first, then a wired one, then the earpiece. Turning the loudspeaker off brings the call back to the headset, and a headset switched on or off mid-call takes the call or hands it back.
- iOS: answering a call checks the microphone, as placing one does: a
refused microphone ends the call
failedafteranswered, and one never asked for is asked for while the call connects. - Android: a repeated push for the call in progress is ignored, as on iOS,
instead of being reported
missed. - Android: a microphone that stops mid-call is brought back once; if it
cannot be, the call ends —
endedwithfailed, "The microphone stopped working." — rather than staying up with the customer silent. - React Native and Flutter:
debugin the calls options turns on the native SDK's call log. - React Native: a call ends a conversation in the assistant bar, and the bar
waits until the call is over — as on iOS, Android and Flutter. It needs
@graineai/inapp-react-native0.40.2 withGraineCalls.attach(client). - React Native:
GraineVoiceBartakes anaccessory— anything that rides on the bar, above it — andGraineBarActiondraws a button for it in the brand's design, such as one that moves the conversation to a call.
Action: upgrade. Nothing in your code has to change; React Native apps that attach the in-app client get the bar handling with both packages updated. Calls are native code, so the upgrade needs a store build.
Flutter 0.6.1 · React Native calls 0.1.0 — calls like WhatsApp, from Flutter and React Native
Use Flutter 0.6.1: 0.6.0 was released without the native call engine and does not build.
- Flutter:
GraineCalls.instanceputs your agent in a native call — CallKit and VoIP pushes on iOS, a ringing call notification and call screen on Android, the call going on with the screen off.handlePushfor your FCM messages,enableVoIPPushesfor the iOS token,outcomesfor your server,attach()for your screen context and actions, and aValueListenable<GraineCallSnapshot>for a call screen of your own. - React Native: the same engine in a new package,
@graineai/inapp-react-native-calls:configure,handlePush,enableVoIPPushes,startCall,onOutcome,attach(client)anduseGraineCall(). callScreenchooses who draws the call while your app is open: the SDK's call screen (the default) or yours.
Action: to add calls, follow In-app calling → React Native or → Flutter. Both carry native code, so they need a new store build. A Flutter app that does not use calls: see Not using calls? — the package now adds the call permissions to your Android manifest.
Android and iOS 0.6.0 — calls like WhatsApp
calls, a native call engine in both SDKs: the customer calls your agent from your app, or your agent rings the customer — answered from the lock screen and going on with the screen off — over Bluetooth too, on iPhone.- iOS: CallKit and VoIP pushes — the system's own incoming-call and call screens.
- Android: a ringing call notification that fills a locked screen, a call screen (the SDK's, or yours) and the call kept alive with the screen off.
- Your server rings a customer with one push, and every outcome —
answered,declined,missed,ended,failed— comes back to your app with yourcall_id.
Action: to add calls, follow In-app calling. On Android, the library now adds the call permissions to your manifest; an app that does not use calls can remove the two Google Play asks about — see Android → Calls.
React Native 0.40.1 — calls that keep going, and clearer voice
endOnBackground={false}onGraineProvidernow keeps the call, as well as the chat, going when the app leaves the foreground. The default is unchanged: a call ends when the app is put away.- On the bring-your-own-audio path (
useGraineVoice): the agent plays at its natural speed, interruptions are recognised while the agent's reply is still playing, and captions keep the spaces between the agent's phrases.
Action: upgrade. Nothing to change in your code.
Captions and variables — live, no upgrade
- Captions read as they were said: the agent's phrases keep their spaces, and the customer's live caption no longer opens with a stray comma.
- Variables reach the call however late they are set: a name passed to
identify()after sign-in, orsetVariables()just before a call, reaches the agent on WebRTC calls too.
New guide: In-app calling.
WebRTC on websites, and your own UI on every platform — live, no upgrade
- High quality (WebRTC) now works on your website. The Graine loader vouches
for your site automatically. A page that drives the engine itself answers the
engine's
graine:need-ticketmessage — see Your own UI → Website. Do not add Graine's domain to your Allowed domains. - New guides: Your own UI — every state to draw, with code for React Native, Android, iOS, Flutter and a website — and Going live.
Android, iOS and Flutter 0.5.0 — a connect that says where it is, and your logo whole
- The bar says more than "Connecting…" when a connect is slow: your reconnecting words while a fresh connection is made, "Still connecting…" after four seconds, and "Couldn't connect · tap to retry" when nothing could carry the call.
connectStage,slowConnectandcallFailedon the voice state, for a bar of your own.- Your logo is drawn whole on its background.
Action: upgrade. Nothing to change in your code.
Your logo, whole — and six new designs
- The logo is never cropped. It is fitted whole on a background chosen for it — your brand colour for a light logo, white for anything else. The dashboard trims empty margins on upload and picks the background; you can change it under the upload. Live on websites; in apps from React Native 0.40.0 and Android, iOS and Flutter 0.5.0.
- Six new designs in the Garage: Swiss Grid, Precision, Japandi Calm, Fintech Clear, Bauhaus and Gamified — each checked for legibility in light and dark. They reach apps on React Native 0.38.0 or Android, iOS and Flutter 0.3.0 and later with no update.
Connecting on mobile data — live, no upgrade
- A call on mobile data no longer hangs when the phone's network has dropped the idle connection (after sleep, or a switch between 5G and 4G). The engine checks the connection as the customer taps, replaces a dead one at once, and tries once more before moving the call to the standard path. A call that answers with no audio is detected within 8 seconds.
- Typical connect on a dropped connection: about 2 seconds, down from a 32-second wait that ended the call.
React Native 0.40.0 — a connect that says where it is
GraineVoiceBarshows your reconnecting words while a fresh connection is made, "Still connecting…" after four seconds (only over the default words — your own wording is never replaced), and "Couldn't connect · tap to retry" when nothing could carry the call.- The voice API gains
connectStage,slowConnectandcallFailed;timing.slowConnectMsmoves the slow mark.
Action: upgrade. Nothing to change in your code.
React Native 0.39.0 · Android, iOS and Flutter 0.4.0 — windows
- A design can open the chat, the caption and every component as desktop-style windows with a title bar. Desktop Buddy opens in windows; any design can turn it on under Appearance → Interior → Frame. An older SDK draws a plain surface.
Action: nothing to change in your code.
React Native 0.38.0 · Android, iOS and Flutter 0.3.0 — the Garage, on the phone
- The design you choose is the design the phone draws. The SDKs draw the voice bar themselves, element by element as the dashboard's preview shows it.
- The typeface loads itself — the SDK fetches exactly the faces the design
uses. React Native takes expo-font's
Font.loadAsynconce asloadFont. - Icons in the design's style — outline, sharp, filled or duotone.
- React Native: mount
GraineVoiceBarinsideGraineVoiceLauncher, and give the providerloadFont(expo-font) andsvg(react-native-svg) once.GraineLauncherButtondraws the design's launcher. The launcher's API carrieserrorandmicDenied. See React Native. - Android, iOS, Flutter: nothing to change. New:
onCloseon the bar andphaseon the voice;erroron Android;showCaptionsandfontFacesin Flutter. - New design: Desktop Buddy. Designs are resolved on the server, so it reaches apps on these versions with no update.
How a design reaches every phone: Your design.
React Native 0.37.0 · Android, iOS and Flutter 0.2.0 — your agent, in your design
- Choose how the agent looks, everywhere, from the dashboard: fifteen design philosophies in Agent → Embed & Widgets → Appearance, tuned for paint, corners, typeface, launcher glyph, motion and chat bubbles, in light and dark, with every text colour checked for legibility.
- One design, every surface: the launcher, the voice bar, the chat window and every component — on your website at once, and in apps on these versions. A component's own look still wins where you set one.
- Fonts: the system font or one of 24 curated families, ten with Devanagari.
- New in the SDKs: a launcher in the design's shape and glyph;
useGraineTheme()in React Native and the resolved tokens onGraineInAppnatively, for anything you draw yourself.
React Native 0.36.0 · Android, iOS and Flutter 0.1.1 — back in the app, ready to talk
- The engine gets ready again the moment the app returns to the foreground, so
the first tap after the app was in the background connects as fast as any
other. If you host the engine page in your own web view, send
graine:warmon return — see Into the page.
Action: nothing to change in your code.
Faster connect — live, no upgrade
From the tap to the call connecting, measured from India:
| Before | Now | |
|---|---|---|
| Engine ready before the tap | 0.47–0.64 s | 0.21–0.33 s |
| Engine not ready yet | 2.26 s | 1.54–1.66 s |
The microphone opens once per call, and the network path is prepared before the tap.
Android, iOS and Flutter 0.1.0 — native SDKs
- An SDK for every platform an app is built with:
Android (
ai.graine:inapp-android), iOS (theGraineInAppSwift package) and Flutter (graine_inapp), alongside React Native — screen context, actions, prompt variables, components, the loader, the draggable keyboard-aware bar, friction detection and connection quality. - The same rules on every platform: what is masked, what "weak connection" means, how a component looks — tested identically on all four. Masking holds in every script, including Devanagari next to an identifier.
- On Android, a call follows the customer between activities: attach the engine once.
React Native 0.35.0 — the network, named; variables everywhere
- Connection quality:
excellent · good · poor · lostonuseGraineConnection(), on the launcher'sapi.quality, and as aconnection_qualityevent. Offline mid-call holds the call asreconnectingfor up to 30 s and resumes it when the network returns. Show "weak connection" and "no connection" in your own bar. - A call that answers with no audio moves to the standard path within
seconds, reported as
onTransportwithreason: "media". - On the call record: transport, fallbacks, quality changes and network loss appear in Execution Logs under What happened in the app.
- Variables reach the agent on every channel — the dashboard's defaults,
the provider's
variables,setVariables()at runtime andidentify(), in that order. New:useGraineVariables(). - The chat is never hidden behind the keyboard, and your own bar can rise
with it:
useKeyboardLift(). - The customer can move the bar — drag it, and it stays where it was left:
draggable,placementandonPlacementChangeonGraineAgentBar, anduseDraggablePlacement()for your own bar. - New load timing options:
stallMs,bootMsandhardCapMs.
React Native 0.34.2 — no stray markers in the chat
- The in-app chat, captions and transcripts no longer show the
<beginning_of_stream>/<end_of_stream>markers.
Action: upgrade.
React Native 0.34.1 — it loads on a weak network
- A slow load is no longer cancelled while it is still downloading; only a load that has stopped moving is retried.
- The engine loads from the nearest edge, and the page is half the size.
Action: upgrade. Nothing to change in your app.
React Native 0.34.0 — the call and the chat, and the agent knows your name
- Prompt variables reach the agent: pass
variablestoGraineProvider, oridentify(), and a prompt that opens withHi {first_name}greets the customer by name — on the chat and the call. - "What visitors get" reaches the app:
GraineAgentBardraws a switch when both the call and the chat are allowed, opens on the dashboard's choice, and hides the microphone on a chat-only agent.
- One conversation runs at a time: switching to the chat ends the call.
autoConnect={false}besideGraineVoiceLauncheris no longer required.
React Native 0.33.6 — a loader is a loader
- A component that is loading shows the loader, centred, on the surface the component will take; showing the component's shape is optional (Appearance → also show the shape). A full-screen wait has a close button.
- The loader never flashes: it stays for at least 450 ms once shown.
- The bar shows your chosen loader while the agent is starting, and hides controls that cannot act yet.
- A bar built on
useGraineAgentshows the live call state.
React Native 0.33.5 — a tap can open a screen, and waiting has a face
- Components: five looks (Gallery, Cards, List, Spotlight, Compact) with
image size, density, buttons, facts, columns, surface, corners, accent and
platform style; When they tap → Open a screen with a path your
open_screenhandler takes; Choose → Several; and a component closed without a choice is reported to the agent (closed/reopen()onuseGraineWidget). GraineLoaderdraws the loader chosen in Appearance → Loading, andapi.phasesays which moment the call is in:idle · connecting · reconnecting · thinking · speaking · listening.- Loads retry on their own and report
connectivity.state; ending a call always ends it and firesonEndedexactly once.timingmakes both configurable. - A tap on a component is part of the conversation — the agent knows what was chosen.
React Native 0.31.0 — ready before the tap, and a call never outlives its UI
- With the launcher mounted early, the engine prepares itself as soon as it loads — the agent starts in about a second.
- A call ends when the screen showing it goes away.
- A component the agent drew while your panel was closed is waiting when the
panel opens (
useGraineWidget);client.pendingWidgetandclient.clearPendingWidget()are there to drive it directly.
Action: nothing to change in your code.
React Native 0.30.0 — pressing Next with blanks, and product lists
- A new friction rule,
incompleteSubmit: the agent speaks up when someone taps "next" or "submit" while a field is still blank or rejected.
- A new component,
product_list: a picture, a title, up to three facts and a price per row, with a button on each — built once in Embed & Widgets, shown on websites and in apps. - A root-relative
imageUrlresolves on a device. - Components are chosen per agent under Embed & Widgets → On screen.
optionalon a field keeps it out of what the agent counts as missing.
Friction rules → · Showing things on screen →
React Native 0.29.0 — the agent knows what comes next
journey.nextonuseGraineScreentells the agent the next step, so it never invents one:
- The agent is told what is still outstanding on the screen — "2 of 4 still need attention: PAN number, Date of birth" — from the fields you already report. What the agent is told →
React Native 0.28.1 — captions that stay
- A caption no longer goes blank a few seconds after it appears.
React Native 0.28.0 — the transcript belongs to the SDK
- The CC button works between turns:
api.captions(every turn, oldest first, up to 12),api.caption(the current turn),api.captionsOn/setCaptionsOn(default on) andclearCaptions().
- The transcript is cleared when a call starts and when you set
active={false}. - Activating with
autoStartreportsconnecting. onCaptionis unchanged. Captions and hanging up →
React Native 0.27.6 — not published
Folded into 0.28.0: active={false} always ends the call and clears
connected / connecting.
React Native 0.27.5 — load the engine before the customer needs it
GraineVoiceLaunchergainsactive(defaulttrue). Mount it early withactive={false}and flip it totruewhen your UI opens, so opening the bar does not wait for the engine to load. How long connecting takes →
React Native 0.27.4 — tap capture reaches your own root
GraineTouchCaptureandGraineFabare exported from the package entry: mount capture at your root to track taps on every screen. Screens outside the provider →- Includes the 0.27.3 fix below.
React Native 0.27.3 — not published
Folded into 0.27.4: the agent no longer loses its actions on a screen that names none.
React Native 0.27.2 — faster connect from the FAB
GraineFabloads the engine when it opens and keeps it ready, so a tap goes straight to the call.
React Native 0.27.1 — the FAB overlay is a prop
- The FAB's iOS overlay is a prop, so the SDK no longer needs a native module and ships over the air.
Do not install 0.27.0
It cannot be delivered over the air. Use 0.27.1 or later.
React Native 0.27.0 — the provider mounts the FAB
fab on GraineProvider, captureTaps, and useGraineInit(). Do not
install — use 0.27.1.
React Native 0.26.2 — a screen change keeps the screen
- Editing a field or re-rendering no longer clears the screen the agent knows about. Fast navigation →
React Native 0.26.1 — the screen after sleep
- The agent knows the current screen when a phone wakes from sleep.
Earlier
0.23.0 – 0.26.0 — the first published SDK, the screen-context contract,
friction detection, and in-app actions. Start here →

