> ## Documentation Index
> Fetch the complete documentation index at: https://www.courier.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Courier Flutter SDK

> Add in-app notifications, push notifications, and notification preferences to your Flutter app with prebuilt widgets and Dart APIs.

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](#inbox)** — prebuilt notification center widget with theming and custom rendering
* **[Push Notifications](#push-notifications)** — automatic token syncing and delivery tracking for APNS and FCM
* **[Preferences](#preferences)** — prebuilt widget for users to manage their notification settings

Available on
<Link href="https://github.com/trycourier/courier-flutter"><Icon icon="github" iconType="solid" /> GitHub</Link>
and <Link href="https://pub.dev/packages/courier_flutter"><Icon icon="box" iconType="solid" /> pub.dev</Link>.

| Requirement     | Value  |
| --------------- | ------ |
| Min Dart SDK    | 3.3.0  |
| Min Flutter     | 3.13.6 |
| Min iOS version | 15.0   |
| Min Android SDK | 23     |
| Gradle          | 8.4+   |

<Warning>
  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.
</Warning>

## Installation

```bash theme={null}
flutter pub add courier_flutter
```

### iOS Setup

Update your deployment target to iOS 15+, then install the CocoaPod:

```bash theme={null}
cd ios && pod install
```

### Android Setup

<Steps>
  <Step title="Add the Jitpack repository">
    In your `android/build.gradle`:

    ```gradle theme={null}
    allprojects {
        repositories {
            google()
            mavenCentral()
            maven { url 'https://jitpack.io' }
        }
    }
    ```
  </Step>

  <Step title="Set minimum SDK version">
    In your `app/build.gradle`:

    ```gradle theme={null}
    minSdkVersion 23
    targetSdkVersion 33
    compileSdkVersion 33
    ```
  </Step>

  <Step title="Gradle sync">
    Your app must support at least Gradle 8.4.
  </Step>
</Steps>

## 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.

<Note>
  For a full walkthrough of JWT generation, see the [Inbox Authentication guide](/platform/inbox/authentication).
</Note>

<Steps>
  <Step title="Generate a JWT on your backend">
    Call the [Issue Token endpoint](/api-reference/authentication/create-a-jwt) from your server:

    ```bash theme={null}
    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"
      }'
    ```
  </Step>

  <Step title="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.

    ```dart theme={null}
    await Courier.shared.signIn(
        userId: "your_user_id",
        accessToken: jwt,
    );
    ```
  </Step>

  <Step title="Sign out when done">
    ```dart theme={null}
    await Courier.shared.signOut();
    ```
  </Step>
</Steps>

### Authentication state

```dart theme={null}
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`.

<Tip>
  Inbox requires the [Courier Inbox provider](https://app.courier.com/channels/courier) to be enabled in your workspace. If using JWT authentication, enable JWT support in the provider settings.

  <img src="https://mintcdn.com/courier-4f1f25dc/iZIqSLNN7hLm8RQn/assets/sdks/mobile/courier-jwt-toggle.png?fit=max&auto=format&n=iZIqSLNN7hLm8RQn&q=85&s=214922b19e181b120f70ed4850b69b9a" alt="JWT toggle in Courier provider settings" width="385" style={{ borderRadius: "8px" }} data-path="assets/sdks/mobile/courier-jwt-toggle.png" />
</Tip>

<Info>
  For an overview of how Courier Inbox works and how to send messages to it from your backend, see [Get Started with Inbox](/platform/inbox/inbox-overview) and [Send an Inbox Message](/platform/inbox/sending-a-message).
</Info>

### Prebuilt Widget

<Frame caption="Default Inbox on iOS and Android">
  <div style={{ display: "flex", gap: "16px", justifyContent: "center" }}>
    <img src="https://mintcdn.com/courier-4f1f25dc/iZIqSLNN7hLm8RQn/assets/sdks/mobile/flutter-inbox-default-ios.png?fit=max&auto=format&n=iZIqSLNN7hLm8RQn&q=85&s=d034503fc2476be86a06363b5ebd588e" alt="Default Inbox on iOS" width="300" style={{ borderRadius: "8px" }} data-path="assets/sdks/mobile/flutter-inbox-default-ios.png" />

    <img src="https://mintcdn.com/courier-4f1f25dc/iZIqSLNN7hLm8RQn/assets/sdks/mobile/flutter-inbox-default-android.png?fit=max&auto=format&n=iZIqSLNN7hLm8RQn&q=85&s=b6e2f493d2534b71669b4a20cccac050" alt="Default Inbox on Android" width="300" style={{ borderRadius: "8px" }} data-path="assets/sdks/mobile/flutter-inbox-default-android.png" />
  </div>
</Frame>

```dart theme={null}
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.

```dart theme={null}
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) { ... },
)
```

<Frame caption="Styled Inbox on iOS and Android">
  <div style={{ display: "flex", gap: "16px", justifyContent: "center" }}>
    <img src="https://mintcdn.com/courier-4f1f25dc/iZIqSLNN7hLm8RQn/assets/sdks/mobile/flutter-inbox-styled-ios.png?fit=max&auto=format&n=iZIqSLNN7hLm8RQn&q=85&s=94352a699443b790fb4243b35924ea93" alt="Styled Inbox on iOS" width="300" style={{ borderRadius: "8px" }} data-path="assets/sdks/mobile/flutter-inbox-styled-ios.png" />

    <img src="https://mintcdn.com/courier-4f1f25dc/iZIqSLNN7hLm8RQn/assets/sdks/mobile/flutter-inbox-styled-android.png?fit=max&auto=format&n=iZIqSLNN7hLm8RQn&q=85&s=476b38f80ec3aaf146a517c07e6e1cad" alt="Styled Inbox on Android" width="300" style={{ borderRadius: "8px" }} data-path="assets/sdks/mobile/flutter-inbox-styled-android.png" />
  </div>
</Frame>

You can also apply branding from [Courier Studio](https://app.courier.com/designer/brands). 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:

```dart theme={null}
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();
```

<Frame caption="Custom Inbox list item rendering">
  <img src="https://mintcdn.com/courier-4f1f25dc/iZIqSLNN7hLm8RQn/assets/sdks/mobile/flutter-inbox-custom.png?fit=max&auto=format&n=iZIqSLNN7hLm8RQn&q=85&s=6765d1ffac0e413d3a35aa1bed23f7d5" alt="Custom Inbox list item rendering" width="300" style={{ borderRadius: "8px" }} data-path="assets/sdks/mobile/flutter-inbox-custom.png" />
</Frame>

### Message Actions

Track every event a Courier message can receive:

```dart theme={null}
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

```dart theme={null}
// 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).

<Note>
  Push notifications require a physical device. Simulators and emulators do not reliably support push token registration or notification delivery.
</Note>

| Feature                    | iOS | Android |
| -------------------------- | --- | ------- |
| Automatic token management | Yes | Yes     |
| Notification tracking      | Yes | Yes     |
| Permission requests        | Yes | No      |

### Provider Setup

Configure your push provider in the [Courier dashboard](https://app.courier.com/channels):

* **iOS**: [APNS](https://app.courier.com/channels/apn) (recommended) or [FCM](https://app.courier.com/channels/firebase-fcm)
* **Android**: [FCM](https://app.courier.com/channels/firebase-fcm) (recommended)

<Info>
  For step-by-step provider credential setup, see the [APNS integration guide](/external-integrations/push/apple-push-notification) or [FCM integration guide](/external-integrations/push/firebase-fcm).
</Info>

### iOS Push Setup

<Steps>
  <Step title="Set the iOS deployment target">
    Open your iOS project and bump the minimum deployment target to **iOS 15.0+**, then re-install pods:

    ```bash theme={null}
    cd ios && pod update
    ```
  </Step>

  <Step title="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.

    ```swift theme={null}
    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)
      }
    }
    ```
  </Step>

  <Step title="Enable the Push Notifications capability">
    In Xcode: select your target > Signing & Capabilities > add **Push Notifications**.
  </Step>

  <Step title="Add a Notification Service Extension (recommended)">
    Required for tracking delivery when the app is not running. See the [iOS SDK push setup](/sdk-libraries/ios#notification-service-extension) 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**.

    <Frame caption="Reorder build phases so Embedded Foundation Extensions runs before Run Scripts">
      <img src="https://mintcdn.com/courier-4f1f25dc/iZIqSLNN7hLm8RQn/assets/sdks/mobile/flutter-xcode-foundation-extensions.png?fit=max&auto=format&n=iZIqSLNN7hLm8RQn&q=85&s=48f9e1d8bfdc2b2333e83c00b2fabf3a" alt="Xcode Build Phases reorder" width="640" style={{ borderRadius: "8px" }} data-path="assets/sdks/mobile/flutter-xcode-foundation-extensions.png" />
    </Frame>
  </Step>
</Steps>

### Android Push Setup

<Warning>
  **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`.
</Warning>

<Steps>
  <Step title="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`:

    ```gradle theme={null}
    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.
  </Step>

  <Step title="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.

    ```kotlin theme={null}
    import com.courier.courier_flutter.CourierFlutterActivity

    class MainActivity : CourierFlutterActivity() {
        // ...
    }
    ```
  </Step>

  <Step title="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.

    ```kotlin theme={null}
    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)
        }
    }
    ```
  </Step>

  <Step title="Register the service in AndroidManifest.xml">
    ```xml theme={null}
    <service
        android:name=".YourNotificationService"
        android:exported="false">
        <intent-filter>
            <action android:name="com.google.firebase.MESSAGING_EVENT" />
        </intent-filter>
    </service>
    ```
  </Step>
</Steps>

### Manual Token Syncing

Use this when you don't want automatic syncing or when you're integrating with a non-FCM provider.

```dart theme={null}
// 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.

```dart theme={null}
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).

```dart theme={null}
// 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](/api-reference/send/send-a-message) with `push` as the routing channel. See the [APNS sending guide](/external-integrations/push/apple-push-notification#sending-messages) or [FCM sending guide](/external-integrations/push/firebase-fcm#sending-messages) for complete examples.

## Preferences

Courier Preferences provides a prebuilt widget for users to manage which notification topics and channels they subscribe to.

<Info>
  Topics and sections are configured in the [Preferences Editor](/platform/preferences/preferences-editor). See [Preferences Overview](/platform/preferences/preferences-overview) for how preference enforcement works at send time.
</Info>

<Frame caption="Default Preferences on Flutter">
  <img src="https://mintcdn.com/courier-4f1f25dc/iZIqSLNN7hLm8RQn/assets/sdks/mobile/flutter-preferences-default.gif?s=8394a00f01c109c5064f5ebbcf036b37" alt="Default Preferences on Flutter" width="250" style={{ borderRadius: "8px" }} data-path="assets/sdks/mobile/flutter-preferences-default.gif" />
</Frame>

```dart theme={null}
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](https://app.courier.com/designer/brands) is automatically applied when a `brandId` is provided.

```dart theme={null}
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,
)
```

<Frame caption="Styled Preferences on Flutter">
  <img src="https://mintcdn.com/courier-4f1f25dc/iZIqSLNN7hLm8RQn/assets/sdks/mobile/flutter-preferences-styled.gif?s=795cde4e2558cfdd51fc6f29386f07e9" alt="Styled Preferences on Flutter" width="250" style={{ borderRadius: "8px" }} data-path="assets/sdks/mobile/flutter-preferences-styled.gif" />
</Frame>

## 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

```dart theme={null}
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

```dart theme={null}
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

```dart theme={null}
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

```dart theme={null}
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

```dart theme={null}
final brand = await client.brands.getBrand(brandId: "...");
```

### URL Tracking

Pass any tracking URL found inside a push notification payload or inbox message:

```dart theme={null}
// 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](https://www.courier.com/docs/reference).

<CardGroup cols={2}>
  <Card title="Inbox Overview" href="/platform/inbox/inbox-overview" icon="inbox">
    Learn about Courier Inbox and how to set it up
  </Card>

  <Card title="Push Integrations" href="/external-integrations/push/intro-to-push" icon="mobile">
    Configure APNS, FCM, and other push providers
  </Card>

  <Card title="Preferences" href="/platform/preferences/preferences-overview" icon="list">
    Set up notification preference topics and channels
  </Card>

  <Card title="GitHub" href="https://github.com/trycourier/courier-flutter" icon="github">
    Source code, examples, and changelog
  </Card>
</CardGroup>
