ebchat

Embeddable chat for Flutter apps, on the ebchat first-party livechat protocol. A drop-in chat screen with host-owned push and multi-company support.

v5 is a clean rewrite. It shares no API with the deprecated GetStream-based ebchat 3.x/4.x.

Install

dependencies:
  ebchat: ^5.0.0

Credentials

You need two values from your ebchat dashboard, under Settings → API keys:

Value What it is
apiKey An eb_… token scoped to the LIVECHAT permission
partnerId The partner id the key was issued under

A LIVECHAT-scoped key can only reach /livechat/v1 — it cannot read your inbox, your customers, or your integrations. That is what makes it safe to ship inside a mobile binary. Do not reuse a full-scope dashboard key here.

Quick start

final chat = await EbChat.init(
  baseUrl: Uri.parse('https://api.enable.tech'),
  apiKey: 'eb_...',                    // LIVECHAT-scoped token
  partnerId: 'your-app',
  user: EbChatUser.guest(),            // or EbChatUser.identified(...)
  language: 'en',                      // 'en' | 'ar'
);

Navigator.of(context).push(MaterialPageRoute(
  builder: (_) => Scaffold(body: EbChatScreen(chat: chat)),
));

EbChatScreen opens (or resumes) the conversation, renders it with the company's theme and language, and transparently starts a fresh conversation when an operator closes the current one. Realtime is used automatically when the gateway advertises it (the bootstrap realtime block), otherwise it polls.

Call chat.dispose() when you tear the instance down.

Resuming across restarts

By default the guest session lives in memory, so a restart starts a new conversation. Pass a SessionStore to persist it — the example app ships a shared_preferences one (example/lib/prefs_session_store.dart):

final chat = await EbChat.init(
  baseUrl: base, apiKey: key, partnerId: partner,
  sessionStore: PrefsSessionStore(),
);

Identified users

When the company enables identity verification, compute the HMAC on your backend — never ship the secret in the app:

// hmac = HMAC_SHA256(userId, companySecret)  — from your server
final chat = await EbChat.init(
  baseUrl: base, apiKey: key, partnerId: partner,
  user: EbChatUser.identified(id: 'crm-123', name: 'Sara', email: '…', hmac: hmac),
);

Identified users get a stable, company-scoped guest id, so the same person keeps one conversation history across devices and reinstalls.

Botflow form questions

When a flow asks the guest something — a name, a delivery location, a photo of a prescription — it arrives as a form question and EbChatScreen answers it for you: text questions focus the composer, and media/location questions upload and send whatever your picker returns.

Two hooks are all it needs. The SDK bundles no picker plugins, so the camera, file browser and GPS stack stay out of hosts whose flows never ask for them:

EbChatScreen(
  chat: chat,
  onPickFile: (request) async {
    // request.kind is EbChatFormKind.photo or .file
    final shot = await ImagePicker().pickImage(source: ImageSource.camera);
    if (shot == null) return null;            // cancelled — question stays open
    return EbChatPickedFile(
      bytes: await shot.readAsBytes(),
      filename: shot.name,
      mimeType: shot.mimeType ?? 'image/jpeg',
    );
  },
  onResolveLocation: () async {
    final fix = await Geolocator.getCurrentPosition();
    return GeoLocation(latitude: fix.latitude, longitude: fix.longitude);
  },
)

example/lib/form_pickers.dart is a complete implementation with permission handling — copy it. The example's Info.plist and AndroidManifest.xml show the entries image_picker and geolocator require.

Both hooks are optional. Omit onPickFile and media questions fall back to your onAttach picker, then to the composer; omit onResolveLocation, or return null from it when permission is denied, and the guest is asked to type the address instead. Every question stays answerable by typing, so a missing hook slows the flow rather than stalling it.

Pass onForm to take over form handling entirely — you then own sending the answer through the controller.

Push notifications

ebchat does not bundle Firebase. Pushes are delivered through your app's Firebase project ("notification outsourcing"): you own firebase_messaging, and ebchat sends through the service-account credentials you upload to your ebchat company settings. Nothing arrives until those credentials are in place.

What the gateway sends

Every bot or operator reply on the guest's conversation produces a notification + data message:

{
  "notification": { "title": "Sara",              // operator name, or the company for bot replies
                    "body":  "Your order is ready" },
  "data": { "type": "ebchat.message.new",
            "companyId": "...", "conversationId": "...",
            "messageId": "...", "sentAt": "2026-08-10T09:12:44.010Z" }
}

Because the payload carries notification, the OS displays it on its own when your app is backgrounded or terminated — you do not have to build a local notification to get the basic "you have a reply" behaviour. The data block is what you use to route a tap. On Android the messages are tagged per conversation, so a burst of bot replies collapses into one notification instead of stacking.

If the tenant has previews disabled (health, payments), body is a generic "New message" and the text stays off the lock screen.

1. Register the device

Do this after EbChat.init, and again whenever FCM rotates the token:

final token = await FirebaseMessaging.instance.getToken();
if (token != null) {
  await chat.registerPushToken(token,
      platform: Platform.isIOS ? 'ios' : 'android');
}

FirebaseMessaging.instance.onTokenRefresh.listen((t) =>
    chat.registerPushToken(t, platform: Platform.isIOS ? 'ios' : 'android'));

Call chat.unregisterPushToken(token) on logout so a shared device stops receiving the previous user's replies.

2. Route a tap into the conversation

EbChatPush.parse returns null for anything that is not an ebchat push, so it is safe to hand it every message you receive.

// Cold start: the notification that launched the app.
final initial = await FirebaseMessaging.instance.getInitialMessage();
_open(EbChatPush.parse(initial?.data ?? {}));

// Warm start: tapped while the app was in the background.
FirebaseMessaging.onMessageOpenedApp.listen((m) => _open(EbChatPush.parse(m.data)));

void _open(EbChatPush? push) {
  if (push == null) return;
  // push.companyId picks the EbChat instance; push.conversationId is the thread.
  navigatorKey.currentState?.push(MaterialPageRoute(
    builder: (_) => Scaffold(body: EbChatScreen(chat: chatFor(push.companyId))),
  ));
}

3. Foreground behaviour

While your app is in the foreground the OS does not display the banner by default, and it should not while the user is already looking at the chat.

// iOS: let the banner through when the user is elsewhere in the app.
await FirebaseMessaging.instance.setForegroundNotificationPresentationOptions(
  alert: true, badge: true, sound: true);

// Android foreground: raise it yourself only when the chat screen isn't open.
FirebaseMessaging.onMessage.listen((m) {
  final push = EbChatPush.parse(m.data);
  if (push == null || isChatScreenOpen(push.conversationId)) return;
  showLocalNotification(m.notification?.title, m.notification?.body);
});

EbChatScreen receives the message over realtime regardless of push, so an open chat updates whether or not the notification is shown.

4. Platform setup

  • Android — no extra code for the background case; the OS renders it. If you use a custom notification channel, name it ebchat_messages (or remap it in your own handler) so importance settings apply.
  • iOS — enable the Push Notifications capability and Background modes → Remote notifications, upload your APNs key to the same Firebase project, and request permission with FirebaseMessaging.instance.requestPermission(). Without an APNs key, iOS receives nothing.

Multi-company hosts

Keep one EbChat per API key. A push carries companyId, so EbChatPush.parse(...) tells you which instance to open.

Local development

Point baseUrl at your local gateway and use the dev key that make livechat-up provisions (eb_dev_livechat / partner livechat-dev). make livechat-info reprints it. On the Android emulator the gateway is http://10.0.2.2:3000, not localhost.

License

MIT — see LICENSE.

Libraries

ebchat
Embeddable ebchat chat SDK for Flutter, on the first-party livechat protocol.