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:

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.

  1. Turn on the Push Notifications capability in Xcode for the app target.
  2. 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.
  3. 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:

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:

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:

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.