Skip to guide
Macaron Docs

Native support, teams and integrations

Add native customer chat to iOS or React Native, then manage replies, push and subscription workflows in Macaron.

Create, connect, verify

Create an app in Macaron, enable chat in Inbox → Support settings → Chat appearance & cards, and use its public app ID with your deployed HTTPS Macaron origin. Download the Swift or React Native source from the app’s Set up chat page. These are editable source components, not published packages or a dependency on Reception. Test with a synthetic customer before distributing your app.

React Native and Expo

Copy MacaronSupport.tsx into your app and mount it inside your own navigation and safe area. Set active to the screen focus state so a hidden mounted screen does not mark messages read. Persist sessions in secure storage; remount using a key derived from app ID and the signed-in user when either changes. The logoUri and fontFamily props override your configured app logo and font; without a logo no Macaron mark is forced into your app.

import { MacaronSupport, useMacaronUnread, registerMacaronPush } from "./MacaronSupport";

const unread = useMacaronUnread(baseUrl, savedSession);
// Render unread as a badge on your own support entry point.
<MacaronSupport
  key={`${appId}:${userId}`} baseUrl={baseUrl} appId={appId}
  active={isScreenFocused} identityToken={identityToken}
  initialSession={savedSession} onSession={persistSecurely}
  onPaywall={(paywallId, fallbackUrl) => presentYourPaywall(paywallId)}
  onReview={() => requestYourNativeStoreReview()}
/>

SwiftUI client and unread badge

Add MacaronSupportView.swift to an iOS 17+ target. Share one MacaronSupportClient between your support screen and entry-point badge. Outside the chat call refresh() while your app is foregrounded to update the badge without marking messages read. MacaronSupportView marks only loaded messages read while visible and active. Persist onSession in Keychain. Use a different client when switching apps or users.

@StateObject private var support = MacaronSupportClient(
    baseURL: URL(string: "https://YOUR_MACARON_HOST")!,
    appID: "YOUR_APP_ID", initialSession: savedSession,
    identityToken: identityToken, onSession: persistInKeychain
)

// Your navigation entry point:
MacaronUnreadBadge(client: support)
// Your destination:
MacaronSupportView(client: support,
    onPaywall: { paywallID, fallbackURL in presentPaywall(paywallID) },
    onReview: { reviewURL in requestNativeReview() })
// Your foreground refresh task:
await support.refresh()

Verified accounts

In Support settings configure a random identity signing secret of at least 32 characters. Your authenticated backend, after verifying its own login, signs an HS256 JWT containing app_id, user_id and exp no more than ten minutes ahead; optional name, email and metadata appear in the team inbox. Never ship the signing secret to a browser or mobile app. Fetch a fresh identityToken before creating a new chat registration. The server verifies the signature and app ID. Anonymous chat remains available; supplying the same user ID does not merge registrations or grant access to other conversation histories. A conversation token restores only its original registration.

// Your authenticated backend only (Node or Bun):
import { createHmac } from "node:crypto";
const header = Buffer.from(JSON.stringify({ alg: "HS256", typ: "JWT" })).toString("base64url");
const payload = Buffer.from(JSON.stringify({
  app_id: appId, user_id: authenticatedUser.id,
  name: authenticatedUser.name, email: authenticatedUser.email,
  metadata: { plan: authenticatedUser.plan },
  exp: Math.floor(Date.now() / 1000) + 300
})).toString("base64url");
const signature = createHmac("sha256", process.env.MACARON_SUPPORT_IDENTITY_SECRET)
  .update(`${header}.${payload}`).digest("base64url");
const identityToken = `${header}.${payload}.${signature}`;

APNs push

Configure Apple key ID, team ID, bundle ID, P8 private key and the correct APNs environment in Support settings. Macaron stores provider credentials encrypted using the server SECRETS_MASTER_KEY. In your host app enable the Push Notifications entitlement and request permission using UNUserNotificationCenter; then call registerForRemoteNotifications. In didRegisterForRemoteNotificationsWithDeviceToken convert the token to lowercase hex and register it after the chat session exists. Use the same bundle and APNs environment as your signed app. Set the notification-center delegate to route a tap containing macaron_conversation_id to that registration’s chat. The notification contains a generic reply notice and conversation ID, never the conversation token or message text.

// Your app controls when to request permission:
let allowed = try await UNUserNotificationCenter.current()
    .requestAuthorization(options: [.alert, .badge, .sound])
if allowed { await MainActor.run { UIApplication.shared.registerForRemoteNotifications() } }

// From your UIApplicationDelegate callback, with the shared support client:
let token = deviceToken.map { String(format: "%02x", $0) }.joined()
Task { try await support.registerPush(token: token) }
// Before logout/reset or switching customer:
try await support.registerPush(token: token, remove: true)

Expo push

Your Expo app obtains permission and an Expo push token using expo-notifications and its EAS project ID. Configure the optional Expo access token in Macaron if enhanced push security is enabled. After onSession returns a registration, call registerMacaronPush with provider expo and that token. Keep the token and session in secure host storage. Unregister before logout, reset or switching users; otherwise the old registration retains its device association and another registration cannot claim it. Native RN apps without Expo can register an APNs token with provider apns instead.

import * as Notifications from "expo-notifications";

const permission = await Notifications.requestPermissionsAsync();
if (permission.status === "granted" && savedSession) {
  const { data: token } = await Notifications.getExpoPushTokenAsync({ projectId });
  await registerMacaronPush(baseUrl, savedSession, "expo", token);
  // On logout, before deleting savedSession:
  // await registerMacaronPush(baseUrl, savedSession, "expo", token, true);
}
// Route notification.request.content.data.macaron_conversation_id
// to your saved registration; never treat it as an authentication token.

Review and paywall cards

Set an HTTPS App Store review URL, an HTTPS plan fallback URL, and optionally a native paywall ID in Chat appearance & cards. From a conversation choose Send review link or Send plan link. Native components display a card and call your onReview or onPaywall callback; without a callback they open the HTTPS fallback. Use your own StoreKit, RevenueCat, Superwall or Adapty purchase screen in that callback. Macaron does not process native purchases. Opening a card is tracked as an open, not a completed review or payment; callbacks can be subject to the platform’s review prompts and purchase rules.

Apps and team

The owner creates an invitation for the teammate’s exact Macaron login and shares the private link themselves. Links expire in seven days, are single use and only work for that signed-in login. Membership spans all customer inboxes owned by that account. Viewers read; agents reply, close and create fix reports; admins also manage chat settings, providers and automation rules. Only the owner invites, changes roles or removes members. Membership does not grant cloud billing, secret values, machines or fix-run access. Each app keeps its own settings and provider credentials.

Telegram notifications and replies

Create a Telegram bot using BotFather and add it to your private team chat. In Support settings enter its bot token, numeric chat ID, explicit allowed teammate user IDs and a random 32-character webhook secret. Set the bot webhook to the displayed Macaron endpoint with Telegram setWebhook, including secret_token with that same value. Macaron does not configure the webhook automatically. Send a synthetic customer message; the bot posts a notification. An allowed teammate replies to that exact bot message to send a reply into the corresponding customer conversation. Unmapped messages, other chats, other users and bot senders are ignored. Telegram receives the customer name and message text, so enable it only for the team chat you intend.

// Run from your private server environment; never commit the bot token.
await fetch(`https://api.telegram.org/bot${process.env.TELEGRAM_BOT_TOKEN}/setWebhook`, {
  method: "POST", headers: { "content-type": "application/json" },
  body: JSON.stringify({
    url: "https://YOUR_MACARON_HOST/api/support/hooks/YOUR_APP_ID/telegram",
    secret_token: process.env.TELEGRAM_WEBHOOK_SECRET,
    allowed_updates: ["message"]
  })
});

Subscription providers and automations

Connect RevenueCat, Superwall V2 or Adapty using the displayed per-app webhook URL and select production or sandbox explicitly. RevenueCat and Adapty use the exact configured Authorization header value, including any Bearer prefix you choose. Superwall V2 uses its Svix signing secret; signatures must be valid and within five minutes. Adapty’s authenticated empty-object verification is supported. Use the same provider customer ID as your signed user_id. In Subscription automations create an exact provider event-type rule and explicitly enable automatic sending. A rule can send a message, paywall card or review card only to existing verified registrations matching that customer. No AI reply is generated. New rules do not replay old events. Disabled providers stop future deliveries. The inbox shows an observed subscription event, not a validated entitlement, payment or access grant. Keep purchase authorization in your billing provider.

Ordering, retries and delivery activity

Webhook IDs deduplicate for 30 days. RevenueCat and Superwall timestamps prevent older observed events from replacing newer ones; Superwall subscription sequence is also checked when a subscription ID is present. Adapty uses receipt order rather than event_datetime. Supported outbound work persists in the Macaron store with retry state, up to five attempts and a two-minute claim lease. Delivery activity shows pending, retry, accepted or failed. Accepted means APNs, Expo or Telegram accepted the request; device receipt is not proven. Network ambiguity can cause duplicate external notifications. Expo DeviceNotRegistered and APNs 410 unregister stale devices. Server workers must remain running for retries. Rule and notification jobs are not a financial ledger.

API reference

Customer transport uses /api/support: GET apps/:app/config, POST apps/:app/sessions with optional identity_token, GET conversations/:id, POST conversations/:id/messages with text and client_id, POST conversations/:id/read with sequence, and POST conversations/:id/devices with provider, token and optional remove. Customer calls use Authorization: Bearer <conversation token> and no account cookies. Native Swift exposes MacaronSupportClient.refresh(markRead:), send(), registerPush(provider:token:remove:), reset(), session and unreadCount. React Native exports MacaronSupport, useMacaronUnread and registerMacaronPush. Owner integration routes use authenticated /api/macaron/v1/inbox/platform/:app (the dashboard manages authentication). Per-app webhook endpoints are public but require provider authentication. Limits: 50 team members, 50 pending invites per owner, 20 rules and 5000 devices per app, 10000 retained webhook events per app, 1000 conversations per owner and 200 messages per conversation.

Billing, privacy and release checks

Macaron subscription and cloud-computer billing stay in Billing; enabling an integration does not buy third-party services or add cloud credit. Saved answers follow the workspace owner’s Pro entitlement. APNs, Expo, Telegram and subscription services have their own accounts and terms. Provider secrets are encrypted server-side and never returned to the setup form. Customer metadata and conversations remain in your Macaron store; do not include credentials or sensitive payment details. Before releasing, test identity tampering, restored sessions, logged-out device removal, foreground unread badges, background notifications and taps, native review/paywall callbacks, member revocation, duplicate webhooks and provider failures on a real device and a sandbox project. Local compilation and fake-provider tests do not establish delivery to a physical phone.