Docs
Everything you need to ship support in your app
Quickstart
- Create an account and add your app. You get an app key (starts with
dsk_). - Open the app's settings and copy the setup prompt into your coding agent, or follow the steps below.
- Send yourself a message from the app (or the web playground) and reply from the inbox.
SDK source lives in this repo under sdk/ios and sdk/react-native. Push it to GitHub and set NEXT_PUBLIC_SDK_REPO so the setup prompts point at it (currently https://github.com/your-org/deskbell).
The SDKs talk to the hosted server at https://deskbell.lol. Running your own? Pass baseURL (Swift) or baseUrl (React Native) with its address.
iOS (Swift)
Requires iOS 17+ and Swift 6. Add the package in Xcode (File → Add Package Dependencies…) and pick the DeskbellKit product.
import SwiftUI
import DeskbellKit
@main
struct CluckyApp: App {
init() {
Deskbell.configure(appKey: "dsk_your_app_key")
}
var body: some Scene { WindowGroup { ContentView() } }
}
struct SettingsView: View {
@State private var showSupport = false
var body: some View {
Button("Contact support") { showSupport = true }
.sheet(isPresented: $showSupport) { DeskbellChatView() }
}
}UIKit: present(Deskbell.makeChatViewController(), animated: true). Unread replies are available as Deskbell.shared.unreadCount (observable) for a badge.
React Native / Expo
Pure TypeScript with no native module, so it works in Expo Go, dev builds and bare React Native (0.76+). Optional peers: expo-store-review for the in-app review sheet, @react-native-async-storage/async-storage to keep the device ID across launches.
npx expo install @react-native-async-storage/async-storage expo-store-review npm install github:your-org/deskbell#path:sdk/react-native
// app/_layout.tsx
import { DeskbellProvider } from "@deskbell/react-native";
export default function RootLayout() {
return (
<DeskbellProvider appKey="dsk_your_app_key">
<Stack />
</DeskbellProvider>
);
}
// app/support.tsx (present as a modal)
import { DeskbellChat } from "@deskbell/react-native";
export default function Support() {
return <DeskbellChat onOfferPress={(offer) => purchase(offer.productId)} onClose={() => router.back()} />;
}onClose adds a Done button to the chat's title bar. Pass showHeader={false} instead if your navigator already shows a header. Unread replies are on useDeskbell().unreadCount.
Showing the chat as a tab? Tab screens stay mounted when you switch away, so pass the focus state, or the chat keeps marking replies as read and the tab badge never appears:
import { useIsFocused } from "expo-router";
export default function SupportTab() {
return <DeskbellChat active={useIsFocused()} />;
}Identifying users
Every install starts as an anonymous user keyed by a device ID. Call identify after sign-in to attach your own user ID, so all of that person's devices share one thread, and to show plan and custom data next to the conversation.
// Swift
Deskbell.identify(userId: user.id, name: user.name, email: user.email,
plan: "Pro Yearly", metadata: ["revenuecatId": rcId, "alarms": 3])
// React Native
deskbell.identify({ userId: user.id, name: user.name, plan: "Pro Yearly", metadata: { alarms: 3 } });The app key ships inside your app, so treat it as public. Anyone holding it can write to your inbox and claim a userId. Don't put secrets in metadata, and add server-side identity verification (an HMAC of the user ID) before relying on userId for anything sensitive.
Offers & reviews
From the inbox, 💸 sends an offer card with a StoreKit product ID; ⭐ sends a review request (set your App Store ID in app settings first). When the user taps an offer, your handler runs the purchase and reports back:
Deskbell.onOfferTapped = { offer in
Task {
guard let product = try await Product.products(for: [offer.productId]).first else { return }
if case .success = try await product.purchase() {
Deskbell.track("offer_accepted", text: "User upgraded to \(offer.title)")
}
}
}Review requests open Apple's write-review page for your app, and log the tap in the thread.
REST API
The SDKs are thin clients over this API, so you can build your own (Flutter, Android, web). Send X-App-Key on every call, and X-User-Token (from identify) on all but config.
GET /api/v1/config → { config: { title, welcomeMessage, accentColor, appStoreId } }
POST /api/v1/identify { deviceId, userId?, name?, email?, plan?, locale?, platform?, appVersion?, metadata? }
→ { token, config }
GET /api/v1/messages?after=<id> → { status, messages: [{ id, sender, kind, body, payload, authorName, createdAt }] }
POST /api/v1/messages { body?, image?: { data: <base64>, mimeType } }
POST /api/v1/events { type: "offer_accepted" | "review_opened" | <custom>, text? }sender is user, agent, ai or system. kind is text, offer (payload: title, subtitle, cta, productId), review_request (payload: appStoreId) or event. Poll messages every few seconds while the chat is open.
AI replies
Set ANTHROPIC_API_KEY on the server, then turn on AI replies and paste your docs in the app's settings. Claude answers new user messages from those docs only, hands over to your team when it isn't sure, and stays out of a thread once a teammate has replied. Override the model with DESKBELL_AI_MODEL.
Telegram
- Create a bot with @BotFather, turn Group Privacy off, and add it to your team's group.
- Set
TELEGRAM_BOT_TOKENand a randomTELEGRAM_WEBHOOK_SECRETon the server. - Register the webhook once:
curl "https://api.telegram.org/bot$TELEGRAM_BOT_TOKEN/setWebhook" \ -d url=https://deskbell.lol/api/telegram/webhook \ -d secret_token=$TELEGRAM_WEBHOOK_SECRET
Paste the group's chat ID in Settings. Replying to a notification in Telegram sends your reply to the user.
Self-hosting
Deskbell is a Next.js app that runs on Cloudflare Workers: data in D1, chat photos in Workers KV. Create both in your Cloudflare account, put their IDs in wrangler.jsonc, then deploy. Point the SDKs at your server with baseURL / baseUrl.
npm install npx wrangler d1 create deskbell npx wrangler kv namespace create deskbell-uploads npm run db:migrate npm run deploy npx wrangler secret put ANTHROPIC_API_KEY