<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Vidur Saini</title>
    <description>The latest articles on DEV Community by Vidur Saini (@vidur_saini).</description>
    <link>https://dev.to/vidur_saini</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4242316%2F2b7c1861-69e2-4d8b-bfa0-89c1b95d65b9.jpeg</url>
      <title>DEV Community: Vidur Saini</title>
      <link>https://dev.to/vidur_saini</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/vidur_saini"/>
    <language>en</language>
    <item>
      <title>How to market your app on TikTok without ads or creators</title>
      <dc:creator>Vidur Saini</dc:creator>
      <pubDate>Sat, 14 Nov 2026 17:30:13 +0000</pubDate>
      <link>https://dev.to/vidur_saini/how-to-market-your-app-on-tiktok-without-ads-or-creators-16dg</link>
      <guid>https://dev.to/vidur_saini/how-to-market-your-app-on-tiktok-without-ads-or-creators-16dg</guid>
      <description>&lt;p&gt;&lt;em&gt;This guide was originally published on the &lt;a href="https://sendhour.com/blog/how-to-market-app-on-tiktok-without-ads" rel="noopener noreferrer"&gt;Sendhour blog&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Short answer:&lt;/strong&gt; Find posts in your niche that beat their account's usual views, copy their structure (never the footage), and post your version every day for a month, mostly as photo slideshows and short screen demos. Then keep the hooks that get installs, not just views. A solo developer can run this on one or two accounts alongside building the app. An agency or studio runs the same loop per client app, with an account per angle. Pay for ads or creators once you know which post converts, not before.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I build Sendhour, a tool for this exact loop, so keep that in mind. Most of this article works with no tool at all, and I say where Sendhour helps in one section near the end.&lt;/p&gt;

&lt;p&gt;Some context on why I care. My apps stalled while I relied on App Store optimization alone, so I moved to organic short video. I now run about 15 TikTok accounts for my own apps, mostly photo slideshows plus some videos. My best posts have reached around 50,000 views. Most posts do far less, and that is normal.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why organic TikTok works for apps
&lt;/h2&gt;

&lt;p&gt;App Store search only reaches people already looking for something like your app. TikTok shows your post to people who weren't looking, and one post that lands can send more users than everything else you've tried. A developer on r/appdev &lt;a href="https://www.reddit.com/r/appdev/comments/1xp5bf4/this_is_how_i_do_tiktok_marketing_for_my_app/" rel="noopener noreferrer"&gt;described this&lt;/a&gt;: they tried build-in-public, SEO, Product Hunt and Reddit, and then one TikTok that crossed a million views brought more users than all of it combined. That is one person's story, not a promise.&lt;/p&gt;

&lt;p&gt;The question people actually ask is the one in &lt;a href="https://www.reddit.com/r/AppBusiness/comments/1xrg5u4/how_are_you_actually_getting_your_first_1000/" rel="noopener noreferrer"&gt;this r/AppBusiness thread&lt;/a&gt;: how do you get your first 1,000 users with no ad budget? Short video is one of the few answers that costs time instead of money.&lt;/p&gt;

&lt;h2&gt;
  
  
  The playbook, step by step
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Pick two formats that fit your app.&lt;/strong&gt; Most apps do well with one of these:

&lt;ul&gt;
&lt;li&gt;Photo slideshows: a hook on the first slide, a few slides that build a story or a list, the app on the last slide. Cheap to make, no face needed.&lt;/li&gt;
&lt;li&gt;Screen demos: 6 to 10 seconds of the app doing one thing, with a hook written on the first frame. Best when the app has a visible before and after, like scanning a receipt and splitting the bill.&lt;/li&gt;
&lt;li&gt;Voiceover videos: real clips with a voice telling a short story that ends at your app.Start with two, not five, so you can tell which one works.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Find posts that beat their account's usual views.&lt;/strong&gt; Search your niche on TikTok and open the profiles of people posting in it. You're looking for a post that did several times what that account normally gets, ideally 10 times or more. A big account's normal day tells you about the account. A small account's breakout tells you about the format.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Write down the structure.&lt;/strong&gt; For each breakout, note the hook pattern, how many slides or seconds, where the payoff lands, and how it ends. For example: "Slide 1 is a relatable complaint, slides 2 to 5 are tips, slide 6 shows the app as the last tip."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Make your version.&lt;/strong&gt; Same structure, your topic, your app, real photos or your own screen recordings. Never reuse the original footage or text.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Post every day on a schedule.&lt;/strong&gt; One post a day per account is enough to start. Schedule a week at a time so a busy day doesn't break the run.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Add accounts before you add volume.&lt;/strong&gt; Once one account has a format that works, a second account with a different angle (a different audience or a different problem your app solves) teaches you more than a second post a day on the first.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Post the same video on other networks.&lt;/strong&gt; The same file can go to Instagram Reels and YouTube Shorts. It costs nothing, and sometimes the post that flops on TikTok does well somewhere else.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Measure, then repeat what works.&lt;/strong&gt; Covered in the next section.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  What to measure and what to repeat
&lt;/h2&gt;

&lt;p&gt;Views are the first filter, not the goal. Keep a simple log for every post: the date, the format, the hook text, views after two days, and whether installs moved that day. After 30 or so posts, patterns show up. A hook style that keeps getting watched, a slide count that keeps working, a topic that sends installs even with modest views.&lt;/p&gt;

&lt;p&gt;If your app uses RevenueCat or Superwall, Sendhour can keep the installs half of that log for you: it imports your daily installs and revenue and lists the posts published in the 48 hours before each jump. A jump after a post is a lead to test again, not proof the post caused it.&lt;/p&gt;

&lt;p&gt;Then do three things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Remake your best post with a new hook. Keep the body, change the first line.&lt;/li&gt;
&lt;li&gt;Drop the format that never gets past the first second after two weeks.&lt;/li&gt;
&lt;li&gt;Watch views against installs. If a post gets views but no installs, the hook is pulling the wrong audience or the app shows up too late. Show the app earlier, or make the hook about the problem the app solves.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A mistake to avoid: judging the whole channel on the first five posts. New accounts often start slow, and most posts on any account land well below its best. Commit to a month of daily posts before you decide TikTok "doesn't work" for your app.&lt;/p&gt;

&lt;p&gt;Another one: reusing the exact same hook word for word across many accounts. It goes stale fast. Rewrite the first line every time, even when the rest is the same.&lt;/p&gt;

&lt;h2&gt;
  
  
  When paid ads or creators are the better choice
&lt;/h2&gt;

&lt;p&gt;Organic is slow and uneven. Pay instead, or as well, when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;You need results this month.&lt;/strong&gt; Ads buy reach on a timeline; organic doesn't promise one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You already have a post that converts.&lt;/strong&gt; Running a proven organic post as an ad is a much safer bet than paying for a cold one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Your app sells on trust.&lt;/strong&gt; Health, finance and coaching apps often need a real person on camera. If that isn't you, paying creators makes sense.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You have budget and no time.&lt;/strong&gt; Organic costs hours every day. If your hours are worth more than the ad spend, pay.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For most solo developers with no ad budget, organic first is the right order. It finds the format, then money scales it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where Sendhour helps
&lt;/h2&gt;

&lt;p&gt;Everything above works by hand. Sendhour does the slow parts of the loop:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Finding breakouts.&lt;/strong&gt; The &lt;a href="https://sendhour.com/features/viral-library" rel="noopener noreferrer"&gt;viral library&lt;/a&gt; collects TikTok posts from the last 30 days with at least 50,000 views and at least 3 times their account's usual views, plus YouTube Shorts, grouped by niche. It's step 2 done for you every day.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Making your version.&lt;/strong&gt; The studio copies a post's structure and hook, as a slideshow from real photos for 1 credit, or as a short video from real clips with a voiceover for 4 credits a render. It never reuses the original footage.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Posting on a schedule.&lt;/strong&gt; One calendar for many accounts across TikTok, YouTube, Instagram, Threads, Bluesky, X and LinkedIn, through each network's official API.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ideas each morning.&lt;/strong&gt; Autopilot prepares 1 to 3 drafts a day on paid plans. At first nothing posts until you render and schedule it. Once an account's last 10 drafts went out without edits, you can let autopilot post them for that account, each scheduled at least 2 hours ahead with an emailed link to cancel it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Watching rivals.&lt;/strong&gt; Add the accounts you compete with and see each one's posts that beat its own usual views, ready to remake.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Showing the app.&lt;/strong&gt; Give Sendhour your App Store or Google Play link and it saves your app's screenshots; every slideshow can then show one in a phone frame on its last slide, free. With autopilot on, it also drafts a post when you ship a new release.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Measuring.&lt;/strong&gt; Analytics for your own accounts, including TikTok posts made outside Sendhour.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Agents.&lt;/strong&gt; Everything works from the AI of your choice through the &lt;a href="https://sendhour.com/docs" rel="noopener noreferrer"&gt;API, MCP server and CLI&lt;/a&gt;, so an agent can read your numbers and draft next week's posts. Connect it with drafts only and nothing goes out until you approve it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Teammates are unlimited on every plan, each with their own sign-in and a role, so a helper can make drafts without being able to publish. What it doesn't do: no paid ads. Plans start at $29 a month for 5 accounts; 15 accounts are $49. See &lt;a href="https://sendhour.com/pricing" rel="noopener noreferrer"&gt;pricing&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  How long does organic TikTok take to work for an app?
&lt;/h3&gt;

&lt;p&gt;Plan on weeks, not days. Give one format three to four weeks of daily posts before you judge it. Most single posts tell you very little; a run of 20 or 30 shows which hooks get watched.&lt;/p&gt;

&lt;h3&gt;
  
  
  Do I need to show my face?
&lt;/h3&gt;

&lt;p&gt;No. Photo slideshows, screen recordings and voiceover videos all work without a face, and they are the easiest formats to make every day. If your app sells on trust, like coaching or health, a real person on camera can help, and that is where hiring creators starts to make sense.&lt;/p&gt;

&lt;h3&gt;
  
  
  How many posts a day should I make?
&lt;/h3&gt;

&lt;p&gt;One a day per account is a good start and easy to keep up. Consistency matters more than volume in the first month. Add a second account with a different angle before you add a second daily post.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is it fine to copy a viral post?
&lt;/h3&gt;

&lt;p&gt;Copy the structure: the hook pattern, the number of slides, the pacing, where the payoff lands. Never reuse someone else's footage, photos or words. Your version should be about your app and your users.&lt;/p&gt;

&lt;h3&gt;
  
  
  Should I post the same video on Instagram Reels and YouTube Shorts?
&lt;/h3&gt;

&lt;p&gt;Yes, it costs nothing extra and each network finds a different audience. Change the caption to fit the network, and watch which one sends installs.&lt;/p&gt;

</description>
      <category>marketing</category>
      <category>tiktok</category>
      <category>startup</category>
      <category>indiehackers</category>
    </item>
    <item>
      <title>How to add customer support chat to an iOS app (2026)</title>
      <dc:creator>Vidur Saini</dc:creator>
      <pubDate>Sat, 14 Nov 2026 17:30:03 +0000</pubDate>
      <link>https://dev.to/vidur_saini/how-to-add-customer-support-chat-to-an-ios-app-2026-1f6i</link>
      <guid>https://dev.to/vidur_saini/how-to-add-customer-support-chat-to-an-ios-app-2026-1f6i</guid>
      <description>&lt;p&gt;&lt;em&gt;This guide was originally published on the &lt;a href="https://hailbell.com/blog/add-support-chat-to-ios-app" rel="noopener noreferrer"&gt;Hailbell blog&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Short answer:&lt;/strong&gt; If you get a few messages a week, a &lt;strong&gt;mailto link or a contact form&lt;/strong&gt; is enough. For most apps, a &lt;strong&gt;hosted chat SDK&lt;/strong&gt; (Intercom, Crisp, Zendesk, Hailbell) is the right call: an afternoon to install, and it handles the inbox, push for replies and storage for you. &lt;strong&gt;Build your own&lt;/strong&gt; only if chat is the product itself. Whichever you pick, the work that decides whether it helps is the same: a button where people get stuck, a signed user identity, context with every message, push for replies, and deletion when the account goes.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;Building for Android too? The same decisions, in Kotlin, are in &lt;a href="https://hailbell.com/blog/add-support-chat-to-android-app" rel="noopener noreferrer"&gt;how to add support chat to an Android app&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Decide where the button lives
&lt;/h2&gt;

&lt;p&gt;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:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A settings row.&lt;/strong&gt; "Contact support" or "Chat with us", near the top, with an unread badge. This is the one everyone expects.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;After a paywall close.&lt;/strong&gt; Someone who closes the paywall often had a question ("does the yearly plan include X?"). A small "Questions about the plan?" link catches them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Error screens.&lt;/strong&gt; 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.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In SwiftUI, a settings row with a badge looks like this with Hailbell. The calls are the same in any view:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="c1"&gt;// App launch, once&lt;/span&gt;
&lt;span class="nc"&gt;Hailbell&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;configure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;appId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"app_XXXXXXXX"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;// Settings&lt;/span&gt;
&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="nc"&gt;SupportRow&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;View&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nd"&gt;@ObservedObject&lt;/span&gt; &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="n"&gt;support&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Hailbell&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;shared&lt;/span&gt;
    &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;some&lt;/span&gt; &lt;span class="nc"&gt;View&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;Button&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nc"&gt;Hailbell&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;shared&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="n"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nc"&gt;LabeledContent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Chat with us"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;support&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;unreadCount&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nc"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"\(support.unreadCount)"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Don't bury it to cut volume. Support nobody can find turns into one-star reviews and refund requests.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Identify the user, and sign it on your server
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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 &lt;code&gt;userId: "1234"&lt;/code&gt;, 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.&lt;/p&gt;

&lt;p&gt;With Hailbell that's an HS256 JWT signed with the app's identity secret, with &lt;code&gt;sub&lt;/code&gt; set to your user ID:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Your backend (Node, with the jsonwebtoken package)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;jwt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sign&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="no"&gt;HAILBELL_IDENTITY_SECRET&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;algorithm&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'HS256'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;expiresIn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'1h'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// The app, after sign-in&lt;/span&gt;
&lt;span class="nc"&gt;Hailbell&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;identify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;identityToken&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;// On sign-out&lt;/span&gt;
&lt;span class="nc"&gt;Hailbell&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Apps on Firebase Auth can skip the backend and pass the user's Firebase ID token instead, with &lt;code&gt;Hailbell.identify(name:email:firebaseIdToken:)&lt;/code&gt;. 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.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Pass context with every chat
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="nc"&gt;Hailbell&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setMetadata&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"plan"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"trial"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"trialEndsIn"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"3 days"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;// Match purchases from your subscription tool&lt;/span&gt;
&lt;span class="nc"&gt;Hailbell&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setMetadata&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"revenueCatUserId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Purchases&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;shared&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;appUserID&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;// What just happened, shown beside the chat&lt;/span&gt;
&lt;span class="nc"&gt;Hailbell&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;track&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"paywall_decline"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;properties&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"placement"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"onboarding"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nc"&gt;Hailbell&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;track&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"restore_failed"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;properties&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"error"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;localizedDescription&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Hailbell sends &lt;code&gt;appVersion&lt;/code&gt;, &lt;code&gt;os&lt;/code&gt; and &lt;code&gt;locale&lt;/code&gt; by itself. Set &lt;code&gt;superwallUserId&lt;/code&gt; or &lt;code&gt;revenueCatUserId&lt;/code&gt; and each conversation shows the customer's plan, purchases and paywall events. Never put passwords, tokens or card details in metadata.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Push the reply, or nobody sees it
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Turn on the Push Notifications capability&lt;/strong&gt; in Xcode for the app target.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Make an APNs key.&lt;/strong&gt; In Apple Developer, under Certificates, Identifiers &amp;amp; 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.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pass the device token and handle taps&lt;/strong&gt; so a tapped reply opens the chat:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;application&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;UIApplication&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;didRegisterForRemoteNotificationsWithDeviceToken&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;Hailbell&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setPushToken&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;// sandbox or production is read from the build&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nd"&gt;@MainActor&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;userNotificationCenter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;UNUserNotificationCenter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;didReceive&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;UNNotificationResponse&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nc"&gt;Hailbell&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;push&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;                                   &lt;span class="c1"&gt;// campaign pushes&lt;/span&gt;
    &lt;span class="nc"&gt;Hailbell&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;handleNotification&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;notification&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;userInfo&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;// chat replies&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A &lt;strong&gt;Notification Service extension&lt;/strong&gt; 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, &lt;code&gt;HailbellNotificationService&lt;/code&gt;, which you subclass). Never link it into the main app.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Decide who answers, and when
&lt;/h2&gt;

&lt;p&gt;A chat button sets an expectation. Decide how you'll meet it before you ship:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Business hours.&lt;/strong&gt; 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.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;An AI first reply.&lt;/strong&gt; 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.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Label the AI.&lt;/strong&gt; 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.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Where your team answers.&lt;/strong&gt; A web inbox works for a support team. A solo developer answers faster from wherever they already are on their phone.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  6. Handle privacy and account deletion
&lt;/h2&gt;

&lt;p&gt;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.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;deleteAccount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;throws&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;deleteAccount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;       &lt;span class="c1"&gt;// your own backend&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nc"&gt;Hailbell&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;deleteData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;     &lt;span class="c1"&gt;// the chat, images and details&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;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 &lt;code&gt;PrivacyInfo.xcprivacy&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Test on a real iPhone
&lt;/h2&gt;

&lt;p&gt;Run this list on a device, with a TestFlight build at least once:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Send a message from the app, answer it, and see it arrive live with the chat open.&lt;/li&gt;
&lt;li&gt;Close the app, answer again, and check the push arrives and opens the chat when tapped.&lt;/li&gt;
&lt;li&gt;Sign in on a second device and check the same conversation shows up. Sign out and check it's gone.&lt;/li&gt;
&lt;li&gt;Delete a test account and check the chat is gone on your side too.&lt;/li&gt;
&lt;li&gt;Try dark mode, the largest text size and a photo.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Debug builds use Apple's sandbox push environment; TestFlight and App Store builds use production. A token sent to the wrong one fails quietly.&lt;/p&gt;

&lt;h2&gt;
  
  
  The mistake to avoid
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where Hailbell helps, and where it doesn't
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Gaps&lt;/strong&gt; 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.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fits&lt;/strong&gt; Solo developers, small app teams and agencies running several apps who want support inside the app and answered from wherever they already are. &lt;a href="https://hailbell.com/docs#ios" rel="noopener noreferrer"&gt;iOS setup&lt;/a&gt; or &lt;a href="https://hailbell.com/#pricing" rel="noopener noreferrer"&gt;pricing&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The other SDKs assistants usually name are good choices for different teams:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Intercom&lt;/strong&gt; 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.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Crisp&lt;/strong&gt; 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.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Zendesk&lt;/strong&gt; 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.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  Questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Can I just use a mailto link instead of in-app chat?
&lt;/h3&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does App Store review require account deletion to delete support chats?
&lt;/h3&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h3&gt;
  
  
  Do push notifications for chat replies work in the simulator?
&lt;/h3&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h3&gt;
  
  
  Should an AI answer support chats in my app?
&lt;/h3&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h3&gt;
  
  
  Which support chat SDKs work in a native iOS app?
&lt;/h3&gt;

&lt;p&gt;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.&lt;/p&gt;

</description>
      <category>ios</category>
      <category>swift</category>
      <category>swiftui</category>
      <category>mobile</category>
    </item>
    <item>
      <title>How to add customer support chat to an Android app (Kotlin, 2026)</title>
      <dc:creator>Vidur Saini</dc:creator>
      <pubDate>Fri, 13 Nov 2026 06:42:51 +0000</pubDate>
      <link>https://dev.to/vidur_saini/how-to-add-customer-support-chat-to-an-android-app-kotlin-2026-4hih</link>
      <guid>https://dev.to/vidur_saini/how-to-add-customer-support-chat-to-an-android-app-kotlin-2026-4hih</guid>
      <description>&lt;p&gt;&lt;em&gt;This guide was originally published on the &lt;a href="https://hailbell.com/blog/add-support-chat-to-android-app" rel="noopener noreferrer"&gt;Hailbell blog&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Most Android apps handle support with a "Contact us" row that opens an email draft. It works, but it throws away everything you need to help: which version they're on, which device, whether they're paying, and which screen they were stuck on. Worse, the reply lands in an inbox they may never check, so the conversation quietly dies.&lt;/p&gt;

&lt;p&gt;In-app chat fixes both problems. The user writes from inside the app, you see their context next to the message, and your reply comes back as a push notification that opens the thread. This guide walks through what a good Android implementation needs, whichever SDK or backend you use: where to put the entry point, how to identify users safely, how reply push works with Firebase Cloud Messaging, the Android 13 notification permission, Google Play's account deletion rule, and how to test the whole loop.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Decide where the chat lives
&lt;/h2&gt;

&lt;p&gt;Users look for help in two moments: when something just went wrong, and when they're deciding whether to pay. Put entry points in both places.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Settings or profile screen.&lt;/strong&gt; A "Help" or "Chat with us" row. This is where people expect it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Error and empty states.&lt;/strong&gt; If a sync fails or an import produces nothing, a "Need help?" button next to the error catches the user while they still care.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Near the paywall.&lt;/strong&gt; Questions like "does this work on my tablet?" or "can I get a refund?" are common right before a purchase. A small help link next to the plans answers them before the user leaves.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Avoid a floating chat bubble over your whole UI. On mobile it covers content and competes with your own buttons. A clear row or button in the right places is enough.&lt;/p&gt;

&lt;p&gt;In Compose, the entry point is an ordinary button that calls whatever your SDK uses to open the chat screen:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Composable&lt;/span&gt;
&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nc"&gt;HelpRow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;onOpenChat&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kt"&gt;Unit&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;ListItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;headlineContent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nc"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Chat with us"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;supportingContent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nc"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"We reply within a day"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;leadingContent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nc"&gt;Icon&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Icons&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;AutoMirrored&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Outlined&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;HelpOutline&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;modifier&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Modifier&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;clickable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;onClick&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;onOpenChat&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Only promise a reply time you can keep. If you're a solo developer in one time zone, "within a day" is honest; "instantly" isn't.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Initialize once, in &lt;code&gt;Application.onCreate()&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Support SDKs should be configured in your &lt;code&gt;Application&lt;/code&gt; class, not in an &lt;code&gt;Activity&lt;/code&gt;. Android can kill and restart your process at any time, and when it recreates a screen it doesn't re-run your launcher activity. Configuring in &lt;code&gt;Application.onCreate()&lt;/code&gt; means the SDK is ready whichever screen comes back first, including when the user taps a reply notification.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;MyApp&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Application&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;onCreate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;onCreate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nc"&gt;SupportChat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;configure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;appId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;BuildConfig&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="no"&gt;SUPPORT_APP_ID&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(&lt;code&gt;SupportChat&lt;/code&gt; stands in for your SDK's entry point.) Remember to register the class with &lt;code&gt;android:name=".MyApp"&lt;/code&gt; on the &lt;code&gt;&amp;lt;application&amp;gt;&lt;/code&gt; element in your manifest.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Identify users without letting anyone impersonate them
&lt;/h2&gt;

&lt;p&gt;If your app has no accounts, a chat can simply belong to the install. That's fine for many utility apps. But if users sign in, you want one conversation that follows them across a phone and a tablet, and that's where people get identity wrong.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Don't&lt;/strong&gt; identify users by passing a user ID or email straight from the app. Anything the client sends can be changed, so a curious user could swap in someone else's ID and read their conversation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do&lt;/strong&gt; have your backend sign a short token that proves who the user is, and pass that token to the chat SDK. The common pattern is a JSON Web Token (&lt;a href="https://www.rfc-editor.org/rfc/rfc7519" rel="noopener noreferrer"&gt;RFC 7519&lt;/a&gt;) signed with a secret that only your server and the chat provider know, with the &lt;code&gt;sub&lt;/code&gt; claim set to your user ID:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="c1"&gt;// After your own sign-in succeeds&lt;/span&gt;
&lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetchSupportIdentityToken&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;// your backend signs this&lt;/span&gt;
&lt;span class="nc"&gt;SupportChat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;identify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;identityToken&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;// On sign-out, so the next person on this device starts fresh&lt;/span&gt;
&lt;span class="nc"&gt;SupportChat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Never ship the signing secret in the APK. Anything in the app binary can be extracted.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Send context with every conversation
&lt;/h2&gt;

&lt;p&gt;The most useful thing in-app chat gives you over email is context you didn't have to ask for. Attach what helps you answer:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;App version and build number (&lt;code&gt;BuildConfig.VERSION_NAME&lt;/code&gt;, &lt;code&gt;BuildConfig.VERSION_CODE&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Device model and Android version (&lt;code&gt;Build.MODEL&lt;/code&gt;, &lt;code&gt;Build.VERSION.RELEASE&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;The user's plan or entitlement, if you sell subscriptions&lt;/li&gt;
&lt;li&gt;The screen they opened chat from
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nc"&gt;SupportChat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setMetadata&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nf"&gt;mapOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="s2"&gt;"app_version"&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt; &lt;span class="nc"&gt;BuildConfig&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="no"&gt;VERSION_NAME&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s2"&gt;"plan"&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt; &lt;span class="n"&gt;currentPlan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;          &lt;span class="c1"&gt;// "free", "pro_yearly", …&lt;/span&gt;
        &lt;span class="s2"&gt;"opened_from"&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt; &lt;span class="s2"&gt;"export_error"&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Keep it to what you'd actually use. Don't attach location, contacts or anything you wouldn't want to explain in your privacy policy, because whatever you collect has to be declared in Google Play's &lt;a href="https://support.google.com/googleplay/android-developer/answer/10787469" rel="noopener noreferrer"&gt;Data safety section&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Deliver replies with push (and ask for permission properly)
&lt;/h2&gt;

&lt;p&gt;Chat only works if the user finds out you replied. On Android that means Firebase Cloud Messaging (FCM).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Register the token.&lt;/strong&gt; Fetch the FCM registration token at startup and give it to your chat provider. Firebase's docs note the token can change, for example when the app is restored on a new device, reinstalled, or the user clears app data, so also handle &lt;code&gt;onNewToken&lt;/code&gt; in your &lt;code&gt;FirebaseMessagingService&lt;/code&gt; (&lt;a href="https://firebase.google.com/docs/cloud-messaging/android/client" rel="noopener noreferrer"&gt;Firebase docs&lt;/a&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nc"&gt;FirebaseMessaging&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getInstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;addOnSuccessListener&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;
    &lt;span class="nc"&gt;SupportChat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setPushToken&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;AppMessagingService&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;FirebaseMessagingService&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;onNewToken&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;SupportChat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setPushToken&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Ask for the notification permission at the right moment.&lt;/strong&gt; Since Android 13 (API level 33), apps need the &lt;code&gt;POST_NOTIFICATIONS&lt;/code&gt; runtime permission to show most notifications (&lt;a href="https://developer.android.com/develop/ui/views/notifications/notification-permission" rel="noopener noreferrer"&gt;Android docs&lt;/a&gt;). Declare it in the manifest, but don't fire the system prompt on first launch. A natural moment is right after the user sends their first support message: "Want a notification when we reply?" makes the value obvious.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="n"&gt;launcher&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;rememberLauncherForActivityResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nc"&gt;ActivityResultContracts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;RequestPermission&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;granted&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="cm"&gt;/* store the answer; chat still works without it */&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;afterFirstMessageSent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Build&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="no"&gt;VERSION&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="no"&gt;SDK_INT&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;33&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;launcher&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;launch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Manifest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;permission&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="no"&gt;POST_NOTIFICATIONS&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the user says no, chat should still work. They'll see your reply next time they open the chat screen, so show an unread badge on your help row.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use a dedicated notification channel.&lt;/strong&gt; Support replies and marketing pushes are different things. Put replies on their own &lt;a href="https://developer.android.com/develop/ui/views/notifications/channels" rel="noopener noreferrer"&gt;notification channel&lt;/a&gt; ("Support replies") so a user who mutes promotions still hears back from you.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Open the right screen on tap.&lt;/strong&gt; A reply notification should land the user in the conversation, not on your home screen. Pass the conversation ID in the notification's intent and route to the chat from your activity.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Let users delete their data, chat included
&lt;/h2&gt;

&lt;p&gt;If your app lets users create an account, Google Play requires you to offer an in-app path to delete the account and its associated data, and a web link where they can request deletion (&lt;a href="https://support.google.com/googleplay/android-developer/answer/13327111" rel="noopener noreferrer"&gt;Play Console Help&lt;/a&gt;). Support conversations are part of that data.&lt;/p&gt;

&lt;p&gt;When a user deletes their account, call your chat provider's delete or reset method as part of the same flow, and confirm with the provider that conversation history is removed on their side too. Mention support messages in your Data safety answers.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Ask for a review only after you've helped
&lt;/h2&gt;

&lt;p&gt;A solved problem is one of the best moments to ask for a Play Store review, because the user's last experience with you was good. Google's &lt;a href="https://developer.android.com/guide/playcore/in-app-review" rel="noopener noreferrer"&gt;In-App Review API&lt;/a&gt; shows the review card inside your app, with two rules worth knowing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;There's a quota.&lt;/strong&gt; Google Play enforces a time-bound limit on how often the card appears, and Google doesn't publish the exact value. Calling &lt;code&gt;launchReviewFlow&lt;/code&gt; more than once in a short period may simply show nothing, so pick the moment carefully.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No pre-questions.&lt;/strong&gt; Google says your app shouldn't ask "Do you like the app?" or similar questions before or while showing the review card.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So don't gate the request on a "Were you happy?" prompt. Ask once, after a conversation where the user thanked you, and accept that the card may not appear.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. Test the whole loop before release
&lt;/h2&gt;

&lt;p&gt;Run through this checklist on a real device, with a release build:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open chat from each entry point and send a message.&lt;/li&gt;
&lt;li&gt;Confirm the message arrives with version, device and plan attached.&lt;/li&gt;
&lt;li&gt;Sign in on a second device and confirm the same conversation appears.&lt;/li&gt;
&lt;li&gt;Force-stop the app, reply from your side, and confirm the push arrives on the "Support replies" channel.&lt;/li&gt;
&lt;li&gt;Tap the notification and confirm it opens the conversation.&lt;/li&gt;
&lt;li&gt;Deny the notification permission and confirm chat still works with an unread badge.&lt;/li&gt;
&lt;li&gt;Sign out and confirm the next user doesn't see the previous conversation.&lt;/li&gt;
&lt;li&gt;Delete the account and confirm the chat history is gone.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Which SDK should you use?
&lt;/h2&gt;

&lt;p&gt;You can build all of this yourself on Firebase, but most teams use a support SDK so they get the chat UI, inbox and push plumbing for free. When comparing options, check for a native Compose UI (not a web view), a signed-identity option, FCM reply push, a deletion call, and whether pricing is per seat or per monthly active user. Read each vendor's Android docs, not just the marketing page: the minimum SDK level, whether the UI is Compose or a web view, and how push tokens are passed are all spelled out there.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Disclosure: I built &lt;a href="https://hailbell.com" rel="noopener noreferrer"&gt;Hailbell&lt;/a&gt;, an in-app support chat and push tool for iOS, Android and web.&lt;/strong&gt; The Android SDK is a native Jetpack Compose library (minSdk 26) that you configure in &lt;code&gt;Application.onCreate()&lt;/code&gt; with &lt;code&gt;Hailbell.configure(context, "app_…")&lt;/code&gt;, register your FCM token with &lt;code&gt;Hailbell.setPushToken(it)&lt;/code&gt;, and open with &lt;code&gt;Hailbell.open(activity)&lt;/code&gt;. An AI answers first from your docs and real prices, and you finish the reply from the dashboard, Discord or Telegram; the user gets a push when you answer. It's $29/month for up to 10,000 monthly active users, with a 14-day free trial and no card required. Setup details are in the &lt;a href="https://hailbell.com/docs" rel="noopener noreferrer"&gt;docs&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;Good in-app support on Android comes down to a few decisions made carefully: entry points where users get stuck, a signed identity instead of a trusted client ID, context attached automatically, reply push on its own channel with a well-timed permission prompt, and a deletion path that covers chat history. Get those right and support stops being an inbox you dread and becomes the place users decide to stay.&lt;/p&gt;

</description>
      <category>android</category>
      <category>kotlin</category>
      <category>jetpackcompose</category>
      <category>mobile</category>
    </item>
    <item>
      <title>How to post your release notes to X, LinkedIn, Threads and Bluesky from Claude Code</title>
      <dc:creator>Vidur Saini</dc:creator>
      <pubDate>Fri, 13 Nov 2026 06:41:54 +0000</pubDate>
      <link>https://dev.to/vidur_saini/how-to-post-your-release-notes-to-x-linkedin-threads-and-bluesky-from-claude-code-2hoo</link>
      <guid>https://dev.to/vidur_saini/how-to-post-your-release-notes-to-x-linkedin-threads-and-bluesky-from-claude-code-2hoo</guid>
      <description>&lt;p&gt;&lt;em&gt;This guide was originally published on the &lt;a href="https://sendhour.com/blog/post-release-notes-from-claude-code-mcp" rel="noopener noreferrer"&gt;Sendhour blog&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;You shipped v2.3. The changelog is written, the tag is pushed, and now comes the part most solo developers skip: telling anyone. Writing four versions of the same announcement, one for each network's length limit and tone, then remembering to post them at a sensible hour, is exactly the kind of chore that never gets done on release day.&lt;/p&gt;

&lt;p&gt;If you already work in Claude Code, you can hand it that chore. Claude Code can connect to remote tools over the &lt;a href="https://modelcontextprotocol.io/specification/2025-06-18/basic/transports" rel="noopener noreferrer"&gt;Model Context Protocol (MCP)&lt;/a&gt;, and several social media schedulers now run MCP servers. Once one is connected, Claude can read your &lt;code&gt;CHANGELOG.md&lt;/code&gt;, write a post per network, and schedule them, without you leaving the terminal you shipped from.&lt;/p&gt;

&lt;p&gt;This guide covers the whole loop: picking a scheduler, connecting it, the prompt that works, the per-network limits Claude needs to respect, and the guardrails that keep an agent from posting something you'd regret.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you need
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Claude Code&lt;/strong&gt;, installed and signed in.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A scheduler with an MCP server&lt;/strong&gt; that supports the networks you post to. Check that it uses each network's official API, and that it lets you limit what an agent may do (more on that below).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Your social accounts connected inside that scheduler.&lt;/strong&gt; You sign in to X, LinkedIn and the rest on the scheduler's side; Claude Code never sees those passwords.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A changelog&lt;/strong&gt; Claude can read. Any Markdown file works. Short, user-facing bullets ("Exports are 3x faster") make better posts than commit messages ("refactor export worker").&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 1: Choose how much the agent is allowed to do
&lt;/h2&gt;

&lt;p&gt;Before you connect anything, decide whether Claude may &lt;strong&gt;publish&lt;/strong&gt; or only &lt;strong&gt;draft&lt;/strong&gt;. This is the single most important setting in the whole workflow.&lt;/p&gt;

&lt;p&gt;A good scheduler gives you levels like these:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Level&lt;/th&gt;
&lt;th&gt;What the agent can do&lt;/th&gt;
&lt;th&gt;When to use it&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Read only&lt;/td&gt;
&lt;td&gt;List accounts, scheduled posts and analytics&lt;/td&gt;
&lt;td&gt;Asking "what's going out this week?"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Drafts only&lt;/td&gt;
&lt;td&gt;Everything above, plus create drafts you approve&lt;/td&gt;
&lt;td&gt;Your default for launch posts&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Full posting&lt;/td&gt;
&lt;td&gt;Schedule and cancel posts that go out without a check&lt;/td&gt;
&lt;td&gt;Repeat jobs you've already reviewed a few times&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Start with drafts. A language model will occasionally invent a feature, round a number the wrong way or tag the wrong account. A draft costs you one extra click; a wrong public post costs more.&lt;/p&gt;

&lt;p&gt;If the server authenticates with an API key instead of a sign-in, remember that the key usually carries the full permissions of the person who created it. Keep it out of your repo and out of shared config.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Connect the MCP server to Claude Code
&lt;/h2&gt;

&lt;p&gt;Remote MCP servers are added with &lt;code&gt;claude mcp add&lt;/code&gt; and the HTTP transport. Anthropic's &lt;a href="https://code.claude.com/docs/en/mcp" rel="noopener noreferrer"&gt;Claude Code MCP documentation&lt;/a&gt; describes HTTP as the recommended option for remote servers and shows the general shape:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Server that signs you in with OAuth in the browser&lt;/span&gt;
claude mcp add --transport http &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;name&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;url&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;

&lt;span class="c"&gt;# Server that takes an API key as a bearer token&lt;/span&gt;
claude mcp add --transport http &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;name&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;url&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; \
  --header &lt;span class="s2"&gt;"Authorization: Bearer &amp;lt;your-key&amp;gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two details from the same docs are worth knowing:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Scope.&lt;/strong&gt; By default a server is added at &lt;em&gt;local&lt;/em&gt; scope: it loads only in the project where you ran the command. Add &lt;code&gt;--scope user&lt;/code&gt; to make it available in every project, or &lt;code&gt;--scope project&lt;/code&gt; to write it to a &lt;code&gt;.mcp.json&lt;/code&gt; file your team can share. If you use a bearer token, don't put it in a project-scoped &lt;code&gt;.mcp.json&lt;/code&gt; that gets committed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Checking it worked.&lt;/strong&gt; Run &lt;code&gt;claude mcp list&lt;/code&gt; from your shell, or type &lt;code&gt;/mcp&lt;/code&gt; inside a Claude Code session to see each server's status and finish any sign-in.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Then ask Claude something harmless first: &lt;em&gt;"List my connected social accounts."&lt;/em&gt; If you get back your X handle, LinkedIn page and so on, the connection works.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Write the prompt once, reuse it every release
&lt;/h2&gt;

&lt;p&gt;The difference between generic AI posts and ones that sound like you is almost entirely in the prompt. Save something like this as a snippet, or as a &lt;a href="https://code.claude.com/docs/en/slash-commands" rel="noopener noreferrer"&gt;Claude Code slash command&lt;/a&gt; if you release often:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Read CHANGELOG.md and find the section for the latest version.

Write launch posts for:
- X: a thread of 2-4 posts. First post says what shipped and why it
  matters to a user. No hashtags. Link only in the last post.
- LinkedIn: one post, plain language, short paragraphs, link to
  https://example.com/changelog as a link card.
- Threads: one post, conversational.
- Bluesky: one post.

Rules:
- Only mention changes that are in the changelog. Do not invent
  features, numbers or customer names.
- Stay inside each network's character limit.
- No emojis in the first line.

Create these as DRAFTS for Friday 09:00 in my time zone (Asia/Kolkata).
Show me every draft before you finish.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few things make this prompt hold up:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;"Only mention changes that are in the changelog."&lt;/strong&gt; This is your main defense against hallucinated features. Claude is reading a real file, so you can tell it to stay inside that file.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;An explicit time zone.&lt;/strong&gt; "Friday 9am" means different things to you, the scheduler's server and the network. Say the zone, or give a full ISO 8601 time with an offset such as &lt;code&gt;2026-11-20T09:00:00+05:30&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"Show me every draft."&lt;/strong&gt; You want the text in your terminal so you can read it before you open the scheduler to approve.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 4: Respect each network's limits
&lt;/h2&gt;

&lt;p&gt;Claude is good at writing to a length when you tell it the length. It is not reliably good at &lt;em&gt;counting&lt;/em&gt; characters the way each network does, so build some margin in and let the scheduler's validation be the final check.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Network&lt;/th&gt;
&lt;th&gt;Text limit&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;X&lt;/td&gt;
&lt;td&gt;280 characters per post&lt;/td&gt;
&lt;td&gt;X weights some characters, such as many emoji, more heavily and counts links at a fixed shortened length. See &lt;a href="https://docs.x.com/fundamentals/counting-characters" rel="noopener noreferrer"&gt;Counting characters&lt;/a&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Threads&lt;/td&gt;
&lt;td&gt;500 characters&lt;/td&gt;
&lt;td&gt;Meta's API docs note that emoji are counted by their UTF-8 bytes. See &lt;a href="https://developers.facebook.com/docs/threads/posts" rel="noopener noreferrer"&gt;Threads posts&lt;/a&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bluesky&lt;/td&gt;
&lt;td&gt;300 graphemes&lt;/td&gt;
&lt;td&gt;Set in the &lt;code&gt;app.bsky.feed.post&lt;/code&gt; lexicon; a &lt;a href="https://github.com/bluesky-social/atproto/discussions/5171" rel="noopener noreferrer"&gt;proposal to raise it&lt;/a&gt; was opened in June 2026, so check the current limit before relying on more room.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LinkedIn&lt;/td&gt;
&lt;td&gt;3,000 characters&lt;/td&gt;
&lt;td&gt;See LinkedIn's &lt;a href="https://www.linkedin.com/help/linkedin/answer/a528176" rel="noopener noreferrer"&gt;Post and share updates&lt;/a&gt; help page.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Practical rules that follow from this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Aim for about 250 characters on X.&lt;/strong&gt; It leaves room for a link and an emoji without tripping the weighted count.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Put the link where it helps.&lt;/strong&gt; On X, a thread lets the first post carry the message and the last post carry the link. On LinkedIn, a link card is usually cleaner than a raw URL in the text.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Don't reuse the X version on LinkedIn.&lt;/strong&gt; LinkedIn gives you ten times the space; one or two short paragraphs explaining &lt;em&gt;who&lt;/em&gt; the change helps read far better there than a squeezed 280-character line.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One cost to know about: X now prices API usage by consumption (see &lt;a href="https://docs.x.com/x-api/getting-started/pricing" rel="noopener noreferrer"&gt;X API pricing&lt;/a&gt;), so schedulers often cap how many X posts you can send per day or month. A three-post thread uses three posts from that allowance.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: Review, approve, and check what went out
&lt;/h2&gt;

&lt;p&gt;When Claude finishes, you should have one draft per network. Before approving:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Read every draft against the changelog.&lt;/strong&gt; Anything that isn't in the changelog comes out.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Check the accounts.&lt;/strong&gt; If you run more than one X account, make sure the draft is on the right one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Check the time.&lt;/strong&gt; Open the scheduler's calendar and confirm the posts sit where you expect, in your time zone.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Approve.&lt;/strong&gt; Only now does anything become public.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;After the posts go out, you can close the loop from the same terminal: &lt;em&gt;"How did last Friday's release posts do?"&lt;/em&gt; If your scheduler exposes analytics over MCP, Claude can pull views and engagement per post, which is a quick way to learn which networks are worth writing longer versions for next time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Guardrails worth setting up
&lt;/h2&gt;

&lt;p&gt;Agents that post in public deserve a few more safety checks than agents that edit code.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Never let a failed send retry blindly.&lt;/strong&gt; If the connection to a network drops halfway through a send, the post may or may not be live. A scheduler that automatically retries can publish duplicates. Prefer one that marks the post as "check the account" instead.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep the agent's key separate.&lt;/strong&gt; Create a key, or a connected app, just for Claude Code so you can revoke it without touching anything else.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mind MCP output size.&lt;/strong&gt; Claude Code warns when an MCP tool returns more than 10,000 tokens and caps output at 25,000 by default (&lt;a href="https://code.claude.com/docs/en/mcp" rel="noopener noreferrer"&gt;docs&lt;/a&gt;). If you ask for "all my posts from this year," expect it to be truncated; ask for a date range instead.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Follow each network's rules.&lt;/strong&gt; Automation through official APIs is allowed; what you post still has to follow each platform's own policies, including disclosure rules for AI-generated media where they apply.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Which scheduler should you use?
&lt;/h2&gt;

&lt;p&gt;Several schedulers now publish Claude Code setup guides for their MCP servers, including &lt;a href="https://postiz.com/claude-code" rel="noopener noreferrer"&gt;Postiz&lt;/a&gt;, &lt;a href="https://preflight.social/docs/mcp" rel="noopener noreferrer"&gt;Preflight&lt;/a&gt; and &lt;a href="https://bundle.social/claude-code-social-media" rel="noopener noreferrer"&gt;bundle.social&lt;/a&gt;. When comparing them, check four things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Networks.&lt;/strong&gt; Does it cover every network you post to, through the official API?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Permission levels.&lt;/strong&gt; Can you give the agent drafts-only access?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Duplicate protection.&lt;/strong&gt; What happens when a send fails halfway?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pricing model.&lt;/strong&gt; Per account, per channel, per post, or flat? Release posts across four networks add up fast on per-post X pricing.&lt;/li&gt;
&lt;/ol&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Disclosure: I built &lt;a href="https://sendhour.com" rel="noopener noreferrer"&gt;Sendhour&lt;/a&gt;, a social media scheduler that has an MCP server for this exact workflow.&lt;/strong&gt; It posts to TikTok, YouTube, Instagram, Threads, Bluesky, X and LinkedIn through each network's official API. Agents can connect with read-only, drafts-only or full-posting access, and a send that loses contact with a network midway is marked "Check account" rather than retried, so you don't get duplicates. Connecting it to Claude Code is one command (&lt;code&gt;claude mcp add --transport http sendhour https://sendhour.com/mcp --header "Authorization: Bearer sh_your_api_key"&lt;/code&gt;), and the setup for other agents is in the &lt;a href="https://sendhour.com/docs" rel="noopener noreferrer"&gt;API docs&lt;/a&gt;. Plans start at $29/month for 5 accounts. Pick whichever tool fits your networks and budget; the workflow above works the same with any of them.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;The point isn't to let an AI run your social accounts. It's to remove the friction between shipping something and saying so. With an MCP-connected scheduler, a release announcement becomes one saved prompt, one review pass and one click to approve. You'll do it every release instead of every third, and that consistency matters more than any single post.&lt;/p&gt;

&lt;p&gt;Start small: connect a scheduler with drafts-only access, run the prompt on your next release, and edit what comes back. After a few releases you'll know whether the drafts are good enough to trust with more.&lt;/p&gt;

</description>
      <category>claudecode</category>
      <category>mcp</category>
      <category>productivity</category>
      <category>marketing</category>
    </item>
  </channel>
</rss>
