Skip to main content
The Courier Flutter SDK provides prebuilt widgets and APIs for building notification experiences in Dart. It handles authentication, token management, and real-time message delivery across iOS and Android from a single codebase.
  • Inbox — prebuilt notification center widget with theming and custom rendering
  • Push Notifications — automatic token syncing and delivery tracking for APNS and FCM
  • Preferences — prebuilt widget for users to manage their notification settings
Available on GitHub and pub.dev.
RequirementValue
Min Dart SDK3.3.0
Min Flutter3.13.6
Min iOS version15.0
Min Android SDK23
Gradle8.4+
The SDK’s intl dependency (>=0.19.0 <1.0.0) is incompatible with Flutter 3.32+ (Dart 3.7+), which ships intl 1.0. If you encounter dependency resolution errors on newer Flutter versions, pin intl: ^0.19.0 in your app’s pubspec.yaml or wait for an SDK update that widens the constraint.

Installation

flutter pub add courier_flutter

iOS Setup

Update your deployment target to iOS 15+, then install the CocoaPod:
cd ios && pod install

Android Setup

1

Add the Jitpack repository

In your android/build.gradle:
allprojects {
    repositories {
        google()
        mavenCentral()
        maven { url 'https://jitpack.io' }
    }
}
2

Set minimum SDK version

In your app/build.gradle:
minSdkVersion 23
targetSdkVersion 33
compileSdkVersion 33
3

Gradle sync

Your app must support at least Gradle 8.4.

Authentication

All SDK features (Inbox, Push, Preferences) require a signed-in user. Authentication is JWT-based; your backend generates a token and the SDK manages credentials across app sessions.
For a full walkthrough of JWT generation, see the Inbox Authentication guide.
1

Generate a JWT on your backend

Call the Issue Token endpoint from your server:
curl -X POST https://api.courier.com/auth/issue-token \
  -H "Authorization: Bearer $YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "scope": "user_id:YOUR_USER_ID write:user-tokens inbox:read:messages inbox:write:events read:preferences write:preferences read:brands",
    "expires_in": "2 days"
  }'
2

Sign in the user

Pass the JWT to the SDK where you manage user state. Credentials persist across app sessions. If the token expires, generate a new one from your backend and call signIn again; the SDK does not handle token refresh automatically.
await Courier.shared.signIn(
    userId: "your_user_id",
    accessToken: jwt,
);
3

Sign out when done

await Courier.shared.signOut();

Authentication state

final userId = await Courier.shared.userId;
final tenantId = await Courier.shared.tenantId;
final isSignedIn = await Courier.shared.isUserSignedIn;

final listener = await Courier.shared.addAuthenticationListener((userId) {
    print(userId ?? "No user signed in");
});

await listener.remove();

Inbox

Courier Inbox provides a prebuilt notification center widget. It supports theming, custom renderers, and real-time updates. The widget automatically adapts to your app’s Flutter Theme unless you provide a custom CourierInboxTheme.
Inbox requires the Courier Inbox provider to be enabled in your workspace. If using JWT authentication, enable JWT support in the provider settings.JWT toggle in Courier provider settings
For an overview of how Courier Inbox works and how to send messages to it from your backend, see Get Started with Inbox and Send an Inbox Message.

Prebuilt Widget

Default Inbox on iOSDefault Inbox on Android
CourierInbox(
  onMessageClick: (message, index) {
    message.isRead ? message.markAsUnread() : message.markAsRead();
  },
  onActionClick: (action, message, index) {
    print(action);
  },
)
The widget automatically picks up your app’s Flutter theme:
  • Button style from Theme.of(context).elevatedButtonTheme.style
  • Loading/unread indicator color from Theme.of(context).primaryColor
  • Text styles from Theme.of(context).textTheme

Theming

Pass a CourierInboxTheme to override the default styles. This controls fonts, colors, unread indicators, swipe actions, tab styles, and button styles.
final theme = CourierInboxTheme(
    loadingIndicatorColor: Color(0xFF9747FF),
    tabIndicatorColor: Color(0xFF9747FF),
    unreadIndicatorStyle: CourierInboxUnreadIndicatorStyle(
        indicator: CourierInboxUnreadIndicator.dot,
        color: Color(0xFF9747FF),
    ),
    // ... additional style properties
);

CourierInbox(
  canSwipePages: true,
  lightTheme: theme,
  darkTheme: theme,
  onMessageClick: (message, index) { ... },
)
Styled Inbox on iOSStyled Inbox on Android
You can also apply branding from Courier Studio. The SDK supports primary color and footer visibility from your brand settings.

Custom Inbox UI

For full control over rendering, use addInboxListener to receive raw message data and build your own UI:
final listener = await Courier.shared.addInboxListener(
  onLoading: (isRefresh) {
    // Show loading state
  },
  onError: (error) {
    // Handle error
  },
  onUnreadCountChanged: (unreadCount) {
    // Update badge indicators
  },
  onTotalCountChanged: (feed, totalCount) {
    // Track total messages per feed
  },
  onMessagesChanged: (messages, canPaginate, feed) {
    // Replace state with the latest message set for the feed
  },
  onPageAdded: (messages, canPaginate, isFirstPage, feed) {
    // A new page of messages was loaded
  },
  onMessageEvent: (message, index, feed, event) {
    // React to individual InboxMessageEvent values:
    // added, read, unread, opened, clicked, archived
  },
);

// Clean up
await listener.remove();
Custom Inbox list item rendering

Message Actions

Track every event a Courier message can receive:
await Courier.shared.openMessage(messageId: messageId);
await Courier.shared.clickMessage(messageId: messageId);
await Courier.shared.readMessage(messageId: messageId);
await Courier.shared.unreadMessage(messageId: messageId);
await Courier.shared.archiveMessage(messageId: messageId);
await Courier.shared.readAllInboxMessages();

// Or via the message object
await message.markAsOpened();
await message.markAsClicked();
await message.markAsRead();
await message.markAsUnread();
await message.markAsArchived();

Reading the feed

// Adjust how many messages are loaded per page (default: 32)
await Courier.shared.setInboxPaginationLimit(limit: 100);

// Currently fetched messages
final messages = await Courier.shared.feedMessages;
final archived = await Courier.shared.archivedMessages;

// Drive pagination manually
final next = await Courier.shared.fetchNextInboxPage(feed: InboxFeed.feed);

// Pull-to-refresh
await Courier.shared.refreshInbox();

Push Notifications

The SDK simplifies push notification setup with automatic token syncing and delivery tracking for both APNS (iOS) and FCM (Android).
Push notifications require a physical device. Simulators and emulators do not reliably support push token registration or notification delivery.
FeatureiOSAndroid
Automatic token managementYesYes
Notification trackingYesYes
Permission requestsYesNo

Provider Setup

Configure your push provider in the Courier dashboard:
  • iOS: APNS (recommended) or FCM
  • Android: FCM (recommended)
For step-by-step provider credential setup, see the APNS integration guide or FCM integration guide.

iOS Push Setup

1

Set the iOS deployment target

Open your iOS project and bump the minimum deployment target to iOS 15.0+, then re-install pods:
cd ios && pod update
2

Extend CourierFlutterDelegate

In ios/Runner/AppDelegate.swift, import the SDK and inherit from CourierFlutterDelegate. This automatically syncs APNS tokens to Courier and forwards delivery / click events to Dart.
import Flutter
import courier_flutter

@main
@objc class AppDelegate: CourierFlutterDelegate {
  override func application(
    _ application: UIApplication,
    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
  ) -> Bool {
    GeneratedPluginRegistrant.register(with: self)
    return super.application(application, didFinishLaunchingWithOptions: launchOptions)
  }
}
3

Enable the Push Notifications capability

In Xcode: select your target > Signing & Capabilities > add Push Notifications.
4

Add a Notification Service Extension (recommended)

Required for tracking delivery when the app is not running. See the iOS SDK push setup for the full steps; the same template applies to Flutter. After adding the extension, in your project settings move Embedded Foundation Extensions above Run Scripts and below Link Binary With Libraries.
Xcode Build Phases reorder

Android Push Setup

Firebase is now a separate dependency. Starting with Courier Flutter 5.x (built on Courier Android 6.x), the SDK no longer bundles Firebase Messaging as a transitive dependency. Your app must add its own Firebase BoM and firebase-messaging artifact so you can subclass FirebaseMessagingService.
1

Add Firebase to your app

Register your app in Firebase, download google-services.json, and place it in android/app/.In android/app/build.gradle:
plugins {
    // ...
    id "com.google.gms.google-services"
}

android {
    defaultConfig {
        minSdkVersion 24
    }
    compileSdkVersion 34
}

dependencies {
    implementation platform("com.google.firebase:firebase-bom:34.13.0")
    implementation "com.google.firebase:firebase-messaging"
}
Sync Gradle.
2

Extend CourierFlutterActivity

Update your MainActivity (Kotlin) to extend CourierFlutterActivity (or CourierFlutterFragmentActivity if you use a FragmentActivity). This lets the SDK forward push delivery and click events into Flutter.
import com.courier.courier_flutter.CourierFlutterActivity

class MainActivity : CourierFlutterActivity() {
    // ...
}
3

Create a FirebaseMessagingService

Subclass Firebase’s FirebaseMessagingService directly and forward both callbacks to Courier. The SDK caches the FCM token, uploads it to Courier when a user is signed in, and broadcasts delivery events through its event bus.
import com.courier.android.Courier
import com.courier.android.notifications.CourierPushNotificationIntent
import com.courier.android.notifications.presentNotification
import com.google.firebase.messaging.FirebaseMessagingService
import com.google.firebase.messaging.RemoteMessage

class YourNotificationService : FirebaseMessagingService() {

    override fun onMessageReceived(message: RemoteMessage) {
        super.onMessageReceived(message)

        // Build the PendingIntent that opens MainActivity and carries the original payload
        val notificationIntent = CourierPushNotificationIntent(
            context = this,
            target = MainActivity::class.java,
            payload = message
        )

        // Show the notification. Prefer data-only FCM so this service runs even when the app is killed.
        notificationIntent.presentNotification(
            title = message.data["title"] ?: message.notification?.title,
            body = message.data["body"] ?: message.notification?.body
        )

        // Tell the Courier SDK that a push was delivered
        Courier.onMessageReceived(message.data)
    }

    override fun onNewToken(token: String) {
        super.onNewToken(token)

        // Register/refresh the FCM token with Courier
        Courier.onNewToken(token)
    }
}
4

Register the service in AndroidManifest.xml

<service
    android:name=".YourNotificationService"
    android:exported="false">
    <intent-filter>
        <action android:name="com.google.firebase.MESSAGING_EVENT" />
    </intent-filter>
</service>

Manual Token Syncing

Use this when you don’t want automatic syncing or when you’re integrating with a non-FCM provider.
// Known Courier providers
await Courier.shared.setTokenForProvider(
  token: fcmToken,
  provider: CourierPushProvider.firebaseFcm,
);
final fcm = await Courier.shared.getTokenForProvider(
  provider: CourierPushProvider.firebaseFcm,
);

// Any provider, by string key (Expo, OneSignal, Pusher Beams, etc.)
await Courier.shared.setToken(token: 'token_value', provider: 'YOUR_PROVIDER');
final token = await Courier.shared.getToken(provider: 'YOUR_PROVIDER');

Handling Push Events

Register a listener to respond when notifications are delivered or tapped. This is useful for deep linking, analytics, or showing in-app alerts.
final pushListener = await Courier.shared.addPushListener(
  onPushDelivered: (push) {
    print(push);
  },
  onPushClicked: (push) {
    print(push);
  },
);

// Remove the listener when no longer needed (e.g. inside dispose())
pushListener.remove();

Requesting Permission

Prompt the user to allow notifications (iOS shows a system dialog; on Android the call is a no-op below API 33).
// Customize how iOS handles foreground delivery
Courier.setIOSForegroundPresentationOptions(options: [
  iOSNotificationPresentationOption.banner,
  iOSNotificationPresentationOption.sound,
  iOSNotificationPresentationOption.list,
  iOSNotificationPresentationOption.badge,
]);

final status = await Courier.requestNotificationPermission();
final current = await Courier.getNotificationPermissionStatus();

Send a Test Notification

Once you’ve completed the setup above, send a test push using the Send API with push as the routing channel. See the APNS sending guide or FCM sending guide for complete examples.

Preferences

Courier Preferences provides a prebuilt widget for users to manage which notification topics and channels they subscribe to.
Topics and sections are configured in the Preferences Editor. See Preferences Overview for how preference enforcement works at send time.
Default Preferences on Flutter
import 'package:courier_flutter/ui/preferences/courier_preferences.dart';

CourierPreferences(
  mode: TopicMode(),
)

Preference Modes

  • Topic mode (TopicMode()): shows subscription topics the user can toggle on or off
  • Channels mode (ChannelsMode(channels: [push, sms, email])): shows per-channel controls for each topic

Theming

Pass a CourierPreferencesTheme to customize fonts, colors, toggle styles, and section headers. Light and dark themes are both supported, and Courier Studio branding is automatically applied when a brandId is provided.
final theme = CourierPreferencesTheme(
    brandId: "YOUR_BRAND_ID",
    sectionTitleStyle: TextStyle(fontWeight: FontWeight.bold, fontSize: 20),
    topicTitleStyle: TextStyle(fontSize: 18),
    // ... additional style properties
);

CourierPreferences(
  mode: TopicMode(),
  lightTheme: theme,
  darkTheme: theme,
)
Styled Preferences on Flutter

CourierClient

For advanced use cases, CourierClient is a low-level wrapper around the Courier API. Each client holds its own credentials, so you can spin up as many as you need.

Initialization

final client = CourierClient(
  jwt:          "...",          // Optional. Required for most authenticated calls
  clientKey:    "...",          // Optional. Used only for Inbox client-key auth
  userId:       "your_user_id",
  connectionId: "...",          // Optional. Used for the inbox websocket
  tenantId:     "...",          // Optional. Scopes the client to a tenant
  showLogs:     true,           // Optional. Defaults to kDebugMode
);

final options = client.options;

Token Management

final device = CourierDevice(
  appId:        "APP_ID",
  adId:         "AD_ID",
  deviceId:     "DEVICE_ID",
  platform:     "ios",
  manufacturer: "Apple",
  model:        "iPhone 15",
);

await client.tokens.putUserToken(
  token: "...",
  provider: "firebase-fcm",
  device: device, // Optional
);

await client.tokens.deleteUserToken(token: "...");

Inbox

final feed = await client.inbox.getMessages(
  paginationLimit: 25,
  startCursor: null,
);

final archived = await client.inbox.getArchivedMessages(
  paginationLimit: 25,
  startCursor: null,
);

final unreadCount = await client.inbox.getUnreadMessageCount();

await client.inbox.open(messageId: "...");
await client.inbox.click(messageId: "...", trackingId: "tracking_id");
await client.inbox.read(messageId: "...");
await client.inbox.unread(messageId: "...");
await client.inbox.archive(messageId: "...");
await client.inbox.readAll();

Preferences

final prefs = await client.preferences.getUserPreferences(
  paginationCursor: null,
);

final topic = await client.preferences.getUserPreferenceTopic(
  topicId: "...",
);

await client.preferences.putUserPreferenceTopic(
  topicId: "...",
  status: CourierUserPreferencesStatus.optedIn,
  hasCustomRouting: true,
  customRouting: [CourierUserPreferencesChannel.push],
);

Branding

final brand = await client.brands.getBrand(brandId: "...");

URL Tracking

Pass any tracking URL found inside a push notification payload or inbox message:
// Available events: clicked, delivered, opened, read, unread
await client.tracking.postTrackingUrl(
  url: trackingUrl,
  event: CourierTrackingEvent.delivered,
);
See the full Courier API reference at courier.com/docs/reference.

Inbox Overview

Learn about Courier Inbox and how to set it up

Push Integrations

Configure APNS, FCM, and other push providers

Preferences

Set up notification preference topics and channels

GitHub

Source code, examples, and changelog