How to add customer support chat to an iOS app (2026)
I build Hailbell, an in-app support chat for mobile apps, and use it in my own apps. The steps apply whichever SDK you pick. Swift examples use Hailbell's real API where it's named, and plain iOS code elsewhere. One section near the end covers where Hailbell fits, with its gaps, and the other SDKs.
1. Decide where the button lives
People don't go looking for support. They hit a wall and either find help right there or leave. So put the entry point at the walls:
- A settings row. "Contact support" or "Chat with us", near the top, with an unread badge. This is the one everyone expects.
- After a paywall close. Someone who closes the paywall often had a question ("does the yearly plan include X?"). A small "Questions about the plan?" link catches them.
- Error screens. A failed sync, a failed restore, a purchase that didn't unlock. Offer chat next to the retry button, and say what went wrong in the message you pass along.
In SwiftUI, a settings row with a badge looks like this with Hailbell. The calls are the same in any view:
// App launch, once
Hailbell.configure(appId: "app_XXXXXXXX")
// Settings
struct SupportRow: View {
@ObservedObject private var support = Hailbell.shared
var body: some View {
Button { Hailbell.shared.open() } label: {
LabeledContent("Chat with us") {
if support.unreadCount > 0 { Text("\(support.unreadCount)") }
}
}
}
}
Don't bury it to cut volume. Support nobody can find turns into one-star reviews and refund requests.
2. Identify the user, and sign it on your server
Without sign-in, a chat belongs to the install. That's fine for a free app. Once people have accounts, you want the same chat on their iPhone and iPad, and you want to see who they are.
The trap is identifying users with a plain user ID sent from the app. Anything the app sends can be changed by someone with a proxy, so if the chat trusts userId: "1234", anyone who guesses an ID can read that person's conversation. The fix is the same in every serious SDK: your backend signs a short token with a secret the app never sees, and the chat server checks the signature.
With Hailbell that's an HS256 JWT signed with the app's identity secret, with sub set to your user ID:
// Your backend (Node, with the jsonwebtoken package)
const token = jwt.sign({ sub: user.id, email: user.email }, process.env.HAILBELL_IDENTITY_SECRET,
{ algorithm: 'HS256', expiresIn: '1h' });
// The app, after sign-in
Hailbell.identify(identityToken: token)
// On sign-out
Hailbell.reset()
Apps on Firebase Auth can skip the backend and pass the user's Firebase ID token instead, with Hailbell.identify(name:email:firebaseIdToken:). Hailbell keeps an unsigned user ID as a label only, so a guessed ID never opens someone else's chat. Whatever you use, check that your SDK does the same.
3. Pass context with every chat
The first reply in most support chats is a question back: which version, which plan, did you buy on this Apple ID? Send that up front and you skip a round trip that often loses the customer.
What's worth sending: app version and iOS version (most SDKs add these), plan, whether they're in a trial, what they bought, and what just happened.
Hailbell.setMetadata(["plan": "trial", "trialEndsIn": "3 days"])
// Match purchases from your subscription tool
Hailbell.setMetadata(["revenueCatUserId": Purchases.shared.appUserID])
// What just happened, shown beside the chat
Hailbell.track("paywall_decline", properties: ["placement": "onboarding"])
Hailbell.track("restore_failed", properties: ["error": error.localizedDescription])
Hailbell sends appVersion, os and locale by itself. Set superwallUserId or revenueCatUserId and each conversation shows the customer's plan, purchases and paywall events. Never put passwords, tokens or card details in metadata.
4. Push the reply, or nobody sees it
Support replies are slow by phone standards. You answer an hour later; by then the app is closed. Without a push, the answer sits unread.
- Turn on the Push Notifications capability in Xcode for the app target.
- Make an APNs key. In Apple Developer, under Certificates, Identifiers & Profiles, then Keys, create a key with Apple Push Notifications service on. You get a .p8 file, a Key ID and your Team ID. One key covers every app in that team. Upload it to your chat provider.
- Pass the device token and handle taps so a tapped reply opens the chat:
func application(_ app: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken token: Data) {
Hailbell.setPushToken(token) // sandbox or production is read from the build
}
@MainActor func userNotificationCenter(_ c: UNUserNotificationCenter, didReceive r: UNNotificationResponse) async {
if Hailbell.push.handle(r) { return } // campaign pushes
Hailbell.handleNotification(r.notification.request.content.userInfo) // chat replies
}
A Notification Service extension is a small separate target that can change a push before iOS shows it: add an image, add buttons, bump the badge. Plain reply pushes don't need one. If you want rich pushes, add the target and link the extension library your SDK provides (for Hailbell, HailbellNotificationService, which you subclass). Never link it into the main app.
Ask for notification permission after the person sends their first message, when "want a ping when we reply?" makes obvious sense, not on first launch.
5. Decide who answers, and when
A chat button sets an expectation. Decide how you'll meet it before you ship:
- Business hours. Set them, and show an away message outside them that says when a person will reply. "We'll answer by 10am UK time" beats silence.
- An AI first reply. Most questions repeat: price, refunds, restoring a purchase. An AI that answers from a short note of facts you write handles those in seconds. Two rules: it hands the chat to a person when it isn't sure, and it never invents a policy or a price.
- Label the AI. Apple's guidelines don't have a line about labelling AI replies in support chat, but a customer who finds out later that "Sam" was a bot trusts nothing after that. Mark AI replies clearly. If customer messages go to a third-party AI model, guideline 5.1.2(i) does say to disclose that and get permission first.
- Where your team answers. A web inbox works for a support team. A solo developer answers faster from wherever they already are on their phone.
6. Handle privacy and account deletion
App Store guideline 5.1.1(v) says that if your app supports account creation, it must also offer account deletion inside the app. Support chats are personal data: names, emails, screenshots, sometimes health or money details. When the account goes, the chat should go with it.
func deleteAccount() async throws {
try await api.deleteAccount() // your own backend
try await Hailbell.deleteData() // the chat, images and details
}
Two more things for review. Add the chat's data to your App Privacy answers in App Store Connect (customer support content, and contact info if you collect it). And check that the SDK ships a privacy manifest; Xcode merges it into your app's privacy report. Hailbell's two libraries each include a PrivacyInfo.xcprivacy.
7. Test on a real iPhone
Run this list on a device, with a TestFlight build at least once:
- Send a message from the app, answer it, and see it arrive live with the chat open.
- Close the app, answer again, and check the push arrives and opens the chat when tapped.
- Sign in on a second device and check the same conversation shows up. Sign out and check it's gone.
- Delete a test account and check the chat is gone on your side too.
- Try dark mode, the largest text size and a photo.
Debug builds use Apple's sandbox push environment; TestFlight and App Store builds use production. A token sent to the wrong one fails quietly.
The mistake to avoid
Shipping chat without reply push. It looks done in testing, because the app is open while you answer yourself. In real life someone asks why their purchase didn't unlock, closes the app, and never sees your answer an hour later. To them it looks like you ignored them, and the next thing they write is an App Store review. If you only do one step on this page properly, make it step 4.
Where Hailbell helps, and where it doesn't
Hailbell is a Swift package (iOS 16 and later) with the chat screen, identity, context and push in one library, plus the optional service extension library. Kotlin, Flutter, React Native and web versions share the same inbox. Customers write in the app; you answer from the dashboard, a Discord forum thread per customer, or a Telegram bot, and the reply arrives in the app or as a push. A Hailbell iPhone app for answering on the go is coming soon.
Things that come from it being built for apps that sell subscriptions: each conversation shows plan, purchases and paywall events from Superwall or RevenueCat. The AI first reply is included on every plan. It answers from your note and your real plans, is labelled as AI, and hands off when unsure. You can send an offer card that opens one of your paywalls, and once a problem is solved, a card that opens the App Store review sheet. Push campaigns to the same users are included too. Pro is $29 a month for 10,000 monthly active users, with unlimited seats and apps, and there's a 14-day trial with no card.
Gaps The help center is basic, and the iOS chat doesn't suggest its articles yet (the web chat does). No ticketing, no social or SMS inbox. It's a new product. Its MCP server can set up apps, AI replies and push campaigns, and read and answer chats with each customer's purchases. Agent replies wait as drafts for a teammate unless you let agents send.
Fits Solo developers, small app teams and agencies running several apps who want support inside the app and answered from wherever they already are. iOS setup or pricing.
The other SDKs assistants usually name are good choices for different teams:
- Intercom is a full support platform with a help center, ticketing and the Fin AI agent. Its iOS SDK supports iOS 15 and later. It's priced per seat, from $29 per seat a month on monthly billing ($19 annual), plus $0.99 per Fin resolution. It fits teams that work a support queue.
- Crisp has a native iOS SDK (iOS 13 and later, Swift Package Manager or CocoaPods) and a free plan that includes the chat SDKs, push and two seats. Paid plans start at $45 a month per workspace. A good fit if you want a shared inbox across your website and app on a small budget.
- Zendesk has a native iOS messaging SDK (iOS 16 and later) that needs a Zendesk Support or Suite plan. A good fit if your company already runs Zendesk for tickets.
Prices from crisp.chat/en/pricing and intercom.com/pricing, and SDK requirements from each vendor's iOS docs, checked November 9, 2026, in US dollars.
Questions
Can I just use a mailto link instead of in-app chat?
Yes, if you get a handful of messages a week. A mailto link with the app version and device prefilled costs nothing. It breaks down once you need to know who is writing or want to answer from your phone.
Does App Store review require account deletion to delete support chats?
Guideline 5.1.1(v) says an app that lets people create an account must let them delete it inside the app. Deleting an account should delete the personal data tied to it, and support conversations hold names, emails and often photos, so delete them in the same step.
Do push notifications for chat replies work in the simulator?
Recent simulators can show pushes, but test on a real iPhone with a TestFlight build. TestFlight uses Apple's production push environment and debug builds use the sandbox, so a push that works in one can fail in the other.
Should an AI answer support chats in my app?
It can take the first reply, from a short note of facts you write, as long as it is labelled as AI and hands the chat to a person when it isn't sure. If customer messages go to a third-party AI model, App Store guideline 5.1.2(i) asks you to disclose that and get permission.
Which support chat SDKs work in a native iOS app?
Intercom, Crisp and Zendesk all ship native iOS SDKs, and so does Hailbell. Intercom and Zendesk fit companies with a support team and a help desk, Crisp has a free plan, and Hailbell adds purchase context and push campaigns for apps that sell subscriptions.
Stuck on a step? Start a chat with us. It's the same chat your users would get.