How to add AI chat and in-app calls to your website and app: identity first, snippet second
By the end of this guide you'll have one AI agent chatting on your website and inside your app, recognising signed-in customers and turning a chat into a voice call that already knows the conversation. Ten steps, the gotchas we've hit in production, and a checklist.
By the end of this guide you'll have one AI agent chatting on your website and inside your iOS, Android, React Native or Flutter app. It will recognise signed-in customers, know the page or screen they're on, and turn a chat into a voice call (in the browser, in the app or to their phone) that picks up where the chat left off.
Pasting the snippet takes five minutes. The launch is decided around it: which sites and apps may use the install, how your server vouches for a signed-in customer, and what a call inherits from the chat. Get those wrong and your best customer is greeted as a stranger, and the call button starts cold. So: identity first, snippet second.
New to agents? Read the complete guide to AI voice agents first.
Before you start
- A working agent with its knowledge base and tools. If you don't have one, follow how to set up an AI voice agent; the same agent will chat and talk.
- A developer who can add one endpoint to your backend.
- HTTPS on every page where calls should work.
- Your production and staging domains, and your apps' bundle IDs and package names.
- For app notifications, a Firebase project (one covers Android and iOS once your APNs key is uploaded to it).
1. Decide what each surface should do
Write one line per surface before touching settings. A website can show chat with a call button, chat only, or a call-only button with no thread (good for a "talk to sales" page). An app can add chat, in-app calls and incoming calls from your business.
Then decide when voice goes to the phone instead. Our rule of thumb: talk in the app or browser when the person is already in your product; call their phone when they're about to walk away.
2. Create the install and lock it to your sites and apps
In Callaro, each website or app gets an install of the agent with its own public key. The key is public by design, so the allow-lists are the protection: browsers are checked by the page's origin against your allowed websites (exact domains or a subdomain wildcard), native apps by bundle ID or package name.
The trap: an install with no websites and no apps answers anyone. That's handy for a first test and a problem in production, so add staging domains explicitly instead of leaving the lists open "for now".
After launch, the install shows which websites and apps connected, with each one's SDK version: the quickest way to spot an old app build.
3. Sign customers in from your server
For signed-in customers, your server signs a short-lived token (a standard HS256 JWT) with the install's identity secret: your user ID plus, optionally, name, email, phone and a few account attributes. Your page or app fetches it and hands it to the widget or SDK.
Three rules from real deployments:
- The secret never ships. Not in the app bundle, not in page JavaScript. Replacing it invalidates every token signed with the old one at once, so rotate alongside a server deploy.
- Trusted facts go in the token. Signed attributes reach the agent as account details from your business. Page context is labelled as coming from the page, and form answers as unverified.
- Handle login and logout mid-chat. On login, call the identify method with a fresh token and the chat moves to that customer. On logout, call the sign-out method so the next person at a shared computer starts clean.
The payoff: the same user ID is the same person on your website and in every app connected to this agent, linked to their CRM contact by the signed phone or email. For account-only support, you can refuse anonymous chats altogether.
4. Decide how anonymous visitors prove who they are
Typed details never link a chat to an existing customer. If a visitor types a phone number into the pre-chat form and it already belongs to a contact, Callaro keeps it as an unverified claim; otherwise anyone could type your customer's number and read their history. A number nobody owns creates a new lead.
To link a visitor properly, offer a one-time code by email, or by WhatsApp or SMS where those channels are connected. WhatsApp codes need an approved template, which our WhatsApp launch guide covers.
Keep the pre-chat form short; every field is a reason not to start. The widget also records campaign tags and ad click IDs before a single-page app tidies the address bar, so each conversation's source is right in reports.
5. Install the widget on your website
Paste the script before the closing body tag on every page, or add it as a Custom HTML tag in Google Tag Manager. In React or Next.js, load it once in the root layout; it follows client-side route changes itself.
By default the widget takes your page's colours and font. The launcher can be an icon, an ask bar along the bottom, or hidden, with your own button opening the chat through a data attribute.
Three things break browser calls most often:
- Plain HTTP. Browsers expose the microphone only in secure contexts; on an insecure page the media devices API is simply undefined (MDN).
- Your own security headers. A Permissions-Policy that disables the microphone blocks calls without even showing a prompt. Your Content-Security-Policy must also allow the widget's script, its API host and secure WebSocket connections.
- Calls started without a tap. Browsers hold audio until the person interacts with the page (Chrome's autoplay policy). The built-in call button handles this; your own button must start the call from its click handler, never from a timer or page load.
6. Add the SDK to your app
The iOS, Android, React Native and Flutter SDKs each offer a drop-in chat screen or a headless controller for your own UI. Install commands with your key filled in are on the dashboard's SDKs and docs page.
The SDKs deliberately don't add microphone permissions, so apps without calls never ask for them:
- iOS: add a microphone usage description to Info.plist (iOS terminates an app that opens the microphone without one), and the audio background mode if calls should continue with the screen locked.
- Android: add the record-audio and modify-audio-settings permissions to the manifest. The drop-in screen requests the runtime permission when the call button is tapped.
For replies while the chat is closed, upload your Firebase service account to the install and register each device's push token. And a cellular call always wins: the agent call stops when one takes the microphone.
7. Tell the agent where the customer is
A chat that doesn't know the page is a worse search box. The widget reports page switches, single-page apps included; apps call a "set screen" method from their navigation. The agent sees the current page and the few before it, so "this plan" makes sense. Query strings are dropped, so a token in a URL never reaches the model.
Add context for what's true now (cart total, order ID) and events for what just happened (cart updated, checkout failed). Then write prompt rules that use them: "If the last event is checkout failed, ask whether the card was declined before anything else."
Proactive messages can open the conversation on time on page, URL, scroll depth or exit intent, under a daily cap per visitor. Start with one rule where people stall, not five on the homepage.
8. Turn chat into voice without starting cold
When a customer asks to talk, the agent sends a call offer, and the platform, not the model, decides which options appear. "Talk now" needs voice on for the install. "Call my phone" needs a linked contact with a number, a voice agent with a line, and no do-not-call entry; tapping it places the call without another AI turn, and the chat names the number that will ring.
The call inherits the chat: who the person is, form answers, page context, recent events and the last messages, so the voice agent continues instead of asking "how can I help?". Your page can pass values only for variables the agent declares, so it can't make a call claim to be a different customer.
If account details sit behind verification, decide whether a signed-in chat's sign-in counts on the call it starts. It's off by default; switched on, only a sign-in through your server's token carries, and actions needing a second factor still ask for it.
Plan for failure too. If an in-app call drops and can't reconnect, the chat offers "Call my phone instead"; an unanswered phone call is reported in the chat once, with an offer to retry when possible.
When the agent needs a date of birth or PIN, the chat shows a private field and calls show a keypad. The platform checks the answer, so the model never sees it. Custom UIs must opt in to the keypad and never store the digits.
9. Ring the app when your business needs to talk
Call invites ring a customer's app (or a website chat they have open) from the dashboard or the API: a driver confirming an address, an advisor calling back. The invite rings briefly then counts as missed, answering on one device stops the others, and values set on it, such as an order ID, are locked into the call. An hourly cap stops a buggy script ringing someone all afternoon.
The drop-in screens show their own accept and decline sheet. For a full-screen system ring, wire the SDK's invite event into CallKit on iOS or a full-screen intent on Android, and pass your CallKit call's ID to the SDK's audio layer so it doesn't mistake its own call for a phone call.
Use invites for calls the customer expects ("we'll ring you in the app in a minute"), not cold outreach.
10. Test the seams before launch
Most of what we've fixed in production broke between steps. On real devices:
- Load the widget from a domain that isn't on the list. It should refuse.
- Sign in on the website, then open the app. It should be the same conversation.
- Type an existing customer's phone number into the pre-chat form. The agent should know nothing about them.
- Start a call mid-chat and ask "what was I asking about?".
- Play a notification sound during an Android call, then take a real phone call.
- Answer one invite on two devices at once. Exactly one call should start.
Common questions
Is the website widget enough, or do we need the SDKs?
The widget covers websites and browser calls. Add the SDKs when customers live in your app, for push replies and app call invites. Fully custom surfaces suit the API channel.
Does the chat share memory with WhatsApp and phone calls?
Yes, once the chat is linked to a contact by signed identity or a one-time code. With agent memory on, a call knows what was said in chat, and the chat knows what the last call promised. More in our omnichannel explainer.
Launch checklist
- [ ] Each surface's job written down: chat, call-only, in-app calls, invites
- [ ] Allowed websites and apps set on every install, staging included
- [ ] Identity token signed on your server; identify and sign-out wired in
- [ ] One-time codes on for anonymous visitors who need account help
- [ ] Pre-chat form cut to the fields you use
- [ ] HTTPS everywhere; Permissions-Policy and CSP allow microphone, widget and WebSockets
- [ ] App microphone permissions added; Firebase connected for push
- [ ] Page, screen, context and events wired, with prompt rules that use them
- [ ] Call offer, phone fallback and dropped-call path tested
- [ ] Verification carry-over decided for chat-started calls
- [ ] All six seam tests passed on real devices
Get identity and allow-lists right and the rest is configuration. See how the web and app chat channel fits into one agent across every channel, and if CRM data is next, read how to connect your CRM to a voice agent.