Graine AI

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 audio line says unit_failed when it was needed.

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-kmp and GraineInApp are 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. callSetupProblems now reports ios_sdk_older with both versions and the fix, and the same sentence goes to the Xcode console at launch, with or without debug.

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_off names the app. GraineCalls.callSetupProblems said 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, bookingAvailable is false with bookingUnavailableReason rings_not_set_up, and a booking it sends anyway is refused with scheduling_off (reason rings_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 — before memories(), 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() or setIdentityToken(), when no refresher is set, or when Verified users is off. See GraineAgent.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.

  • mode chooses 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 with mode: "voice" shows the chat, and the console says why; voice.start() and chat.* refuse what the agent does not offer here. If your page relied on mode to 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. See GraineAgent.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() or openBooking(), 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 passes load({ preload: true }) (data-preload on the tag, preload on <graine-agent>); a headless load() 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 of load() as a data- attribute, data-variables as JSON, and data-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.blocks lists a Start blank component's blocks in order, each a GraineWidgetBlock with a unique id, a type (GraineWidgetBlockType) and only the field its type uses: content for a heading, text or button, imageUrl for 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's GraineComponent.blocks (0.14.8) is now held to the same rule. See GraineWidgetBlock on iOS and GraineWidgetBlock on 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.style is 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 whose wire is 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. See GraineComponentStyle.
  • What a tap does, as When they tap set it. GraineComponent.onSelect says whether a tap opens a screen (through the one open_screen handler your app registers) or runs one of your actions, with its arguments and the sentence the agent hears; it is null when 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 with Graine.chat.answerWidget, exactly as the SDK's own views do. See GraineComponent.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 unique id; a block of a type this version does not know, or with nothing to draw, is left out. See GraineComponentBlock.
  • 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 your baseUrl, as the SDK's own views draw it.
  • The agent is answering — say so. Graine.componentAwaitingReply (and each agent's) is true from the moment your app answers a component with Graine.chat.answerWidget and 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 to false when the conversation ends, when the component is closed with dismissWidget, and after 12 seconds with no reply, as the SDK's own views stop waiting then. See GraineAgent.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 to identify or setIdentityToken, 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-Id header, 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 says idle, connecting, connected, reconnecting, offline, failed or disconnected — the words Flutter and React Native already used. connected is what online was: .online / GraineChatStatus.ONLINE still compile, as deprecated aliases. A chat that gave up says failed (it said offline), one Graine ended says disconnected (iOS said offline), and offline now means a message waiting with nothing connecting it. Check your code: a switch or when over every case without a default or else needs the new ones, and code that read offline as "gave up" should read failed. See the iOS and Kotlin Multiplatform references.
  • A native call's phase, in one word. GraineCalls.phase (iOS) and Graine.calls.phase (Kotlin Multiplatform) say idle, ringing, calling, connecting, active, reconnecting, ended or missed — the phase Flutter and React Native already have, mapped the same way. See GraineCallPhase (iOS) and GraineCallPhase (Kotlin Multiplatform).
  • reportEvent reaches 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.
  • reportEvent is 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.onError on iOS, a GraineEvent.Error on Graine.events in 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 Native client.startWorkflow, websites GraineAgent.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, add com.google.android.play:integrity); identify(traits, identityToken) with setIdentityToken and an identityTokenRefresher proves who the user is. Debug builds send a debug token (debugTokenMode, debugToken) so they keep working once you enforce. New error codes app_not_allowed, app_not_verified and identity_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 startCall never places a call after it returned null.
  • 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.bookingState is a StateFlow<GraineBookingState> that follows the agent's settings, identify and setMemory: collect it to show your own booking button instead of reading bookingAvailable once at launch. See the Kotlin Multiplatform reference.
  • reportEvent on 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_required names the trait the agent identifies users by; 502 and 504 read as unavailable. On websites a failed load() still resolves false (it never rejects); what the page asked meanwhile, and asks after it, fails at once with the session's own reason, and the next load() 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_loaded and 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): reportEvent and track no 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.com then asha@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 are invalid_request, 401 invalid_key, 403 origin_refused, 404 not_found, 429 rate_limited, 502, 503, 504 and the CDN's 520–524 unavailable — worth trying again — and any other status unknown. 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 name or detail in reportEvent's data never replaces the event's own; cooldown_seconds, max_per_session and quiet sit at the event's level, and everything else travels under its data — the same event in the chat and on a call.
  • Booked calls say why the settings could not load (iOS): scheduledCalls() and cancelScheduledCall(id:) throw that reason instead of answering for this install alone, as Android does.
  • bookingState never keeps a stale answer (Kotlin Multiplatform): a refresh racing a sign-out on another thread can no longer leave it available; and on iOS, booking read on another thread as the settings arrive never says not_identified for 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, when identify() 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's reason. 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's verification show 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. reconnecting is true meanwhile, so GraineVoiceBar shows 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 GraineVoiceBar no 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 rtc cannot be used.
  • Connection quality is the last few seconds. quality is 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 — with refreshPastSessions(), loadMorePastSessions() and sessionTranscript(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 chatCloseDelaySeconds later (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.captions and Kotlin Multiplatform GraineCallInfo.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.

  • 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")), with chat, voice and component actions. 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, and speakerByDefault = false starts 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) and session on 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.androidCallActivity on Android.

  • The web component draws your own UI too: <graine-agent headless>, with the same state and actions.

  • Every SDK: iOS GraineInApp and GraineInAppRTC, Android ai.graine:inapp-android and ai.graine:inapp-android-rtc, Kotlin Multiplatform ai.graine:inapp-kmp and ai.graine:inapp-kmp-compose, Flutter graine_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 GraineInApp and GraineInAppRTC, Android ai.graine:inapp-android and ai.graine:inapp-android-rtc, Kotlin Multiplatform ai.graine:inapp-kmp and ai.graine:inapp-kmp-compose, Flutter graine_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, null under a name removes it (load()'s value for that name, else the agent's default, applies again), and setVariables(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 when load() puts another agent on it. Safe before load().
  • GraineAgent.currentVariables() returns the set the next conversation carries — the agent's defaults, load()'s variables, 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 Native chat_rated).
  • Pin what the chat is about: chat.pin(…) (React Native setChatPin, the website GraineAgent.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 Native onChatAttach) 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 GraineInApp and GraineInAppRTC, Android ai.graine:inapp-android and ai.graine:inapp-android-rtc, Kotlin Multiplatform ai.graine:inapp-kmp and ai.graine:inapp-kmp-compose, Flutter graine_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 reconnecting and 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 GraineInApp and GraineInAppRTC, Android ai.graine:inapp-android and ai.graine:inapp-android-rtc, Kotlin Multiplatform ai.graine:inapp-kmp and ai.graine:inapp-kmp-compose, Flutter graine_inapp, React Native @graineai/inapp-react-native and @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 to startCall; 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. iOS GraineCalls.shared.prepareCall(reason:variables:agent:), Android GraineCalls.prepareCall(reason, variables, agent), Kotlin Multiplatform Graine.calls.prepareCall(reason, variables, agent), Flutter GraineCalls.instance.prepareCall(reason:, variables:, agent:), React Native calls prepareCall({ 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 GraineInApp and GraineInAppRTC, Android ai.graine:inapp-android and ai.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 transportFallbackReason no_answer. 0.10.2's 3 seconds only delayed that fallback. Nothing to change in your app. Every SDK: the native engines (iOS GraineInAppRTC, Android ai.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 transportFallbackReason no_answer. Nothing to change in your app. Every SDK: the native engines (iOS GraineInAppRTC, Android ai.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.json in android.googleServicesFile and the calls package's plugin brings Firebase Cloud Messaging (firebaseMessaging: false leaves it out); in a bare app, graineFirebaseMessaging=true in android/gradle.properties. Flutter: com.google.firebase:firebase-messaging in android/app/build.gradle.kts, no Dart.
  • Already have your own FirebaseMessagingService (or @react-native-firebase/messaging, or firebase_messaging)? It outranks the SDK's and receives every message, so keep handing Graine's pushes and tokens over — handlePush and onNewToken / registerPushToken, as before. See In-app calling.
  • no_push_token now 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), and callPermissions reads 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. callSetupProblems lists 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-calls to plugins in app.json and it adds the iOS microphone sentence, the background modes and the push entitlement; webrtc: true turns 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 GraineInAppRTC package (https://github.com/GraineAI/inapp-ios-rtc, or pod 'GraineInAppRTC'), beside GraineInApp at 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 and graineWebRtc=true in gradle.properties, or GRAINE_WEBRTC=1 in 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.
  • callTransport in each SDK's calls configuration: auto (the default — WebRTC when the module is in the app), websocket or webrtc. transport and transportFallbackReason on 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 also dashboard and unavailable). Objective-C: on GraineObjCCalls. 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 as chat_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: reconnecting on Android and Flutter (Kotlin Multiplatform's RECONNECTING follows it on Android). React Native: useGraineAgent().failed and client.chatFailed (heard as chat_failed) when the chat's first connect failed or Graine ended or refused the conversation, and retry() (hook and client) connects again, the backoff starting over — a network drop is never failed.
  • The captions switch on iOS, Android, Flutter and Kotlin Multiplatform, as React Native's: captionsOn (on by default), setCaptionsOn and clearCaptions(); the SDK's bar hides its caption while it is off, and what was said is kept. Android's voice keeps it in captions (GraineCaption), and Flutter's switch is GraineCaptionsSwitch.
  • The rows a chat window draws, for a chat of your own: iOS GraineChatRows.build(turns) and Android ChatRows.build(messages), as React Native's chatTurnRows, Flutter's ChatRows and Kotlin Multiplatform's GraineChatRows.
  • Answer a component from your own UI: iOS answerComponent(widgetId:data:summary:), Android answerWidget(widgetId, data, summary), Kotlin Multiplatform Graine.chat.answerWidget(id, data, summary). React Native's client.presentWidget(frame) and presentPendingWidget(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's upcomingCalls, refreshUpcomingCalls() and cancelUpcomingCall(id). On iOS these need GraineInApp 0.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's client.getVariables() is documented as what it is: the merged set the agent will receive. Objective-C gains currentVariables, themeJSON, the captions switch, answerWidgetWithId:dataJSON:summary: and the chat's upcoming calls.
  • viewOpened() and viewClosed() on iOS and Android, and Android's voice.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_called and call_reason ride 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 error event fires, and a refused microphone says so: useGraineEvents hears error (message) when the agent or the chat could not start, or the voice launcher reports something to fix (as onError does); a WebRTC voice call the microphone refused reports microphone_denied (it said connect_failed) and mic_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: maskString applies your addMaskRule rules (it ran only the built-in ones), and maskDeep keeps a Date as its ISO string (it became {}).
  • Website: an action handler still running is answered error at 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, GraineBookingView and GraineBookingViewController (Objective-C presentBookingWithReason:completion:); Android presentBooking(activity, reason) { call, error -> } and GraineBookingDialog; Flutter GraineBooking.show(context, reason:) and GraineBookingSheet; React Native openBooking({ reason }) and <GraineBookingSheet>; Kotlin Multiplatform presentBooking(reason); the website GraineAgent.openBooking({ reason }). See Booking from your app with the SDK's sheet.
  • bookingAvailable on every agent — and Flutter's bookingBlocker and bookingListenable: 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, phone or a custom field such as customer_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() and forgetMemory() 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, a GraineProvider per 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, and startCall(…, 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, cancelScheduledCall and schedulingRules, 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 successful forgetMemory() clears the chat too. Each turn carries restored and channel for 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:) and chat.open(size:) on iOS, GraineConfig(chatSize =) and chat.open(size) on Android, GraineConfig(chatSize:) and GraineChat.open(context, size:) on Flutter, GraineConfig(chatSize =) and Graine.chat.open(size) in Kotlin Multiplatform, chatSize, client.openChat({ size }) and GraineChatScreen in React Native, and GraineAgent.open({ mode, size }) and load({ chatSize }) on a website. See Chat → Window size.
  • Kotlin Multiplatform. ai.graine:inapp-kmp is 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(), and GraineChatRows for a chat of your own), the voice bar, calls with each call's agent, scheduled calls and several agents — and ai.graine:inapp-kmp-compose adds 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, removeField and clearScreen are 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_reason and 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 failed after answered, 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 — ended with failed, "The microphone stopped working." — rather than staying up with the customer silent.
  • React Native and Flutter: debug in 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-native 0.40.2 with GraineCalls.attach(client).
  • React Native: GraineVoiceBar takes an accessory — anything that rides on the bar, above it — and GraineBarAction draws 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.instance puts 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. handlePush for your FCM messages, enableVoIPPushes for the iOS token, outcomes for your server, attach() for your screen context and actions, and a ValueListenable<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) and useGraineCall().
  • callScreen chooses 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 your call_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} on GraineProvider now 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, or setVariables() 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-ticket message — 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, slowConnect and callFailed on 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

  • GraineVoiceBar shows 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, slowConnect and callFailed; timing.slowConnectMs moves 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.loadAsync once as loadFont.
  • Icons in the design's style — outline, sharp, filled or duotone.
  • React Native: mount GraineVoiceBar inside GraineVoiceLauncher, and give the provider loadFont (expo-font) and svg (react-native-svg) once. GraineLauncherButton draws the design's launcher. The launcher's API carries error and micDenied. See React Native.
  • Android, iOS, Flutter: nothing to change. New: onClose on the bar and phase on the voice; error on Android; showCaptions and fontFaces in 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 on GraineInApp natively, 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:warm on 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:

BeforeNow
Engine ready before the tap0.47–0.64 s0.21–0.33 s
Engine not ready yet2.26 s1.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 (the GraineInApp Swift 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 · lost on useGraineConnection(), on the launcher's api.quality, and as a connection_quality event. Offline mid-call holds the call as reconnecting for 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 onTransport with reason: "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 and identify(), 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, placement and onPlacementChange on GraineAgentBar, and useDraggablePlacement() for your own bar.
  • New load timing options: stallMs, bootMs and hardCapMs.

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 variables to GraineProvider, or identify(), and a prompt that opens with Hi {first_name} greets the customer by name — on the chat and the call.
  • "What visitors get" reaches the app: GraineAgentBar draws 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.
const { surface, canSwitch, setSurface } = useGraineSurface();
  • One conversation runs at a time: switching to the chat ends the call. autoConnect={false} beside GraineVoiceLauncher is 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 useGraineAgent shows 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_screen handler takes; Choose → Several; and a component closed without a choice is reported to the agent (closed / reopen() on useGraineWidget).
  • GraineLoader draws the loader chosen in Appearance → Loading, and api.phase says 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 fires onEnded exactly once. timing makes 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.pendingWidget and client.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.
friction: {
  incompleteSubmit: { taps: ["next", "submit", "pay"] },   // or false to switch it off
  screens: { review: { incompleteSubmit: false } },        // pressing on here is fine
}
  • 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 imageUrl resolves on a device.
  • Components are chosen per agent under Embed & Widgets → On screen.
  • optional on a field keeps it out of what the agent counts as missing.
fields: [{ name: "middle_name", label: "Middle name", status: "empty", optional: true }]

Friction rules → · Showing things on screen →

React Native 0.29.0 — the agent knows what comes next

  • journey.next on useGraineScreen tells the agent the next step, so it never invents one:
journey: { name: "KYC", step: 3, of: 5, next: "Take a selfie" }
  • 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) and clearCaptions().
{api.captionsOn && api.captions.map((c, i) => (
  <Text key={i} style={{ opacity: c.live ? 0.7 : 1 }}>{c.text}</Text>
))}
<Button title="CC" onPress={() => api.setCaptionsOn(!api.captionsOn)} />
  • The transcript is cleared when a call starts and when you set active={false}.
  • Activating with autoStart reports connecting.
  • onCaption is 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

  • GraineVoiceLauncher gains active (default true). Mount it early with active={false} and flip it to true when 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

  • GraineTouchCapture and GraineFab are 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

  • GraineFab loads 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 →

On this page

iOS and Android 0.14.11, Kotlin Multiplatform 0.14.12 — a call whose audio would not start recoversiOS and Android 0.14.10, Kotlin Multiplatform 0.14.11 — the app says when Xcode links an older iOS SDKiOS and Android 0.14.9, Kotlin Multiplatform 0.14.10 — the calls setup check names the app, not the agentHosted ringing — an app is rung only with its own push credentialsWebsite loader 1.6.1 — a visitor's first conversation waits for the identity token your page is still fetchingWebsite loader 1.6.0 — what the dashboard sets reaches every website, almost nothing loads until the agent is used, and a conversation follows the visitoriOS 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 designKotlin 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 replyiOS, Android and Kotlin Multiplatform 0.14.7 — the next call is prepared with the user's tokeniOS, Android and Kotlin Multiplatform 0.14.6 — a user's first conversation waits for the token your app is still fetchingThe Security log, and guides to secure your agent — live, no upgradeiOS and Kotlin Multiplatform 0.14.5 — the same words for the chat and the call on every SDK, and events that stay under 8 KBiOS, 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 firstThe voice engine keeps WebRTC calls connected — live, no upgradeThe voice engine on weak networks — live, no upgradeiOS, 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 screeniOS, 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 transcriptiOS, 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 callsiOS, Android, Flutter and Kotlin Multiplatform 0.13.1 · React Native calls 0.7.1 — a WebRTC call answered on its first dialPrompt variables while your website is open — live, no upgradeiOS, Android, Flutter and Kotlin Multiplatform 0.13.0 · React Native 0.46.0 — a chat window you design in GarageiOS, Android, Flutter and Kotlin Multiplatform 0.12.0 · React Native 0.45.0 and calls 0.7.0 — a call screen you design in GarageiOS, Android, Flutter and Kotlin Multiplatform 0.11.0 · React Native calls 0.6.0 — calls connect faster, and each call reports how it connectediOS, 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 secondiOS, 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 siOS, 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 writeiOS, 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 missingiOS, 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 SDKiOS, 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 ConnectingiOS, 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 appiOS and Android 0.6.2 · Flutter 0.6.3 — a phone call puts the agent on holdiOS and Android 0.6.1 · Flutter 0.6.2 · React Native 0.40.2 and calls 0.1.1 — calls follow the headsetFlutter 0.6.1 · React Native calls 0.1.0 — calls like WhatsApp, from Flutter and React NativeAndroid and iOS 0.6.0 — calls like WhatsAppReact Native 0.40.1 — calls that keep going, and clearer voiceCaptions and variables — live, no upgradeWebRTC on websites, and your own UI on every platform — live, no upgradeAndroid, iOS and Flutter 0.5.0 — a connect that says where it is, and your logo wholeYour logo, whole — and six new designsConnecting on mobile data — live, no upgradeReact Native 0.40.0 — a connect that says where it isReact Native 0.39.0 · Android, iOS and Flutter 0.4.0 — windowsReact Native 0.38.0 · Android, iOS and Flutter 0.3.0 — the Garage, on the phoneReact Native 0.37.0 · Android, iOS and Flutter 0.2.0 — your agent, in your designReact Native 0.36.0 · Android, iOS and Flutter 0.1.1 — back in the app, ready to talkFaster connect — live, no upgradeAndroid, iOS and Flutter 0.1.0 — native SDKsReact Native 0.35.0 — the network, named; variables everywhereReact Native 0.34.2 — no stray markers in the chatReact Native 0.34.1 — it loads on a weak networkReact Native 0.34.0 — the call and the chat, and the agent knows your nameReact Native 0.33.6 — a loader is a loaderReact Native 0.33.5 — a tap can open a screen, and waiting has a faceReact Native 0.31.0 — ready before the tap, and a call never outlives its UIReact Native 0.30.0 — pressing Next with blanks, and product listsReact Native 0.29.0 — the agent knows what comes nextReact Native 0.28.1 — captions that stayReact Native 0.28.0 — the transcript belongs to the SDKReact Native 0.27.6 — not publishedReact Native 0.27.5 — load the engine before the customer needs itReact Native 0.27.4 — tap capture reaches your own rootReact Native 0.27.3 — not publishedReact Native 0.27.2 — faster connect from the FABReact Native 0.27.1 — the FAB overlay is a propReact Native 0.27.0 — the provider mounts the FABReact Native 0.26.2 — a screen change keeps the screenReact Native 0.26.1 — the screen after sleepEarlier