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

> ## Agent Instructions
> Install the Courier skill before writing code: `npx skills add trycourier/courier-skills`. It carries the verified SDK shapes and the rules you cannot get wrong.
> Authenticate every request with `Authorization: Bearer <API_KEY>`. A workspace has several environments and each has its own keys, which are plain `pk_` strings with no environment prefix. Start with Test.
> Send with `client.send.message` from the Node SDK (`@trycourier/courier` v7 and later, where the client is the default import). Reference a template by its `nt_` id or its alias.
> A send accepts a bare Elemental element list, but storing content on a template requires the top-level elements wrapped in a channel element.
> Templates and journeys can be built in the Courier app or created through the API. Either way they live in the workspace and are referenced by ID when you send.
> The hosted MCP server is https://mcp.courier.com. For a briefing on what Courier is and when to use it, read https://www.courier.com/llms.txt.
> Prefer the Guides tab for how-do-I questions and the Docs tab for how-does-it-behave questions. The API reference lives under /api-reference.

# Set up mobile push

> Connect FCM and APNs, sync device tokens with the mobile SDK, and send a test push.

export const Tags = ({items}) => {
  const routes = {
    Email: "/integrations/email/overview",
    SMS: "/integrations/sms/overview",
    Push: "/integrations/push/overview",
    Inbox: "/in-app/overview",
    Chat: "/integrations/direct-message/overview",
    Templates: "/design/templates/overview",
    Variables: "/design/templates/variables",
    Elemental: "/design/elemental/overview",
    Brands: "/design/brands",
    Translations: "/design/elemental/locales",
    Routing: "/send/routing",
    Preferences: "/recipients/preferences/overview",
    Journeys: "/journeys/overview",
    Broadcasts: "/broadcasts/overview",
    Tenants: "/tenants/overview",
    Logs: "/monitor/overview",
    Webhooks: "/monitor/webhooks/outbound",
    Lists: "/recipients/lists-and-audiences/overview",
    Users: "/recipients/overview",
    Digests: "/journeys/nodes/digest",
    Environments: "/workspaces/overview",
    MCP: "/resources/mcp"
  };
  const icons = {
    Email: "envelope",
    SMS: "comment",
    Push: "mobile",
    Inbox: "inbox",
    Chat: "comments",
    Templates: "pen-ruler",
    Variables: "pen-ruler",
    Elemental: "pen-ruler",
    Brands: "pen-ruler",
    Translations: "pen-ruler",
    Routing: "paper-plane",
    Preferences: "users",
    Journeys: "route",
    Broadcasts: "bullhorn",
    Tenants: "building",
    Logs: "chart-simple",
    Webhooks: "chart-simple",
    Lists: "users",
    Users: "users",
    Digests: "route",
    Environments: "briefcase",
    MCP: "toolbox"
  };
  const base = "https://d3gk2c5xim1je2.cloudfront.net/fontawesome/v7.2.0/regular/";
  const names = String(items || "").split(",").map(entry => entry.trim()).filter(Boolean);
  return <div className="cx-tags">
      {names.map(name => {
    const href = routes[name];
    const icon = icons[name];
    const url = icon ? "url(" + base + icon + ".svg)" : null;
    const style = url ? {
      "--cx-tag-icon": url
    } : null;
    if (!href) {
      return <span className="cx-tag" data-icon={icon} style={style} key={name}>
              {name}
            </span>;
    }
    return <a className="cx-tag" data-icon={icon} style={style} href={href} key={name}>
            {name}
          </a>;
  })}
    </div>;
};

export const Endpoint = ({method, path, name, href, children, bare}) => {
  const verb = String(method || "").toUpperCase();
  const title = verb + " " + path;
  const label = children || name || path;
  if (bare) {
    return href ? <a href={href}><code>{title}</code></a> : <code>{title}</code>;
  }
  if (!href) {
    return <span className="cx-endpoint" data-method={verb} title={title}>
        <span className="cx-endpoint-label">{label}</span>
        <span className="cx-endpoint-method">{verb}</span>
      </span>;
  }
  return <a className="cx-endpoint" data-method={verb} href={href} title={title}>
      <span className="cx-endpoint-label">{label}</span>
      <span className="cx-endpoint-method">{verb}</span>
    </a>;
};

export const AppLink = ({href, children, name, bare}) => {
  const label = children || name || "Open in Courier";
  if (bare) {
    return <a href={href} target="_blank" rel="noreferrer">{label}</a>;
  }
  return <a className="cx-endpoint" data-kind="app" href={href} target="_blank" rel="noreferrer">
      <span className="cx-endpoint-label">{label}</span>
      <span className="cx-endpoint-method" aria-hidden="true">↗</span>
    </a>;
};

export const Doc = ({href, children, name, bare}) => {
  const label = children || name || href;
  if (bare) {
    return <a href={href}>{label}</a>;
  }
  return <a className="cx-endpoint" data-kind="doc" href={href}>
      <span className="cx-endpoint-label">{label}</span>
      <span className="cx-endpoint-method">DOC</span>
    </a>;
};

<Tags items="Push, Templates" />

A push needs three things: a provider connected in Courier, a device token on the user, and a send.

The Courier mobile SDK handles the token. Pick your platform below, work down the steps, then send a test push.

## Prerequisites

* <AppLink href="https://app.courier.com/">A Courier account</AppLink> and an <AppLink href="https://app.courier.com/settings/api-keys">API key</AppLink>
* Provider credentials: a Firebase service account JSON for Android, an APNs `.p8` key for iOS, or both
* A physical device, since simulators and emulators do not reliably receive push

## Set up your app

Pick your platform. Each tab is the complete path, from an empty project to a device that receives push.

<Tabs>
  <Tab title="iOS">
    <Steps>
      <Step title="Connect APNs in Courier">
        Open <AppLink href="https://app.courier.com/integrations">Integrations</AppLink>, switch to the environment you send from, and choose **Apple Push Notification Service**. Providers are per environment, so one connected in test does not exist in production.

        Fill in four fields, then click **Install Provider**:

        * **Key Id**, the identifier of your APNs key
        * **Key**, the contents of the downloaded `.p8` file
        * **Team Id**, your Apple Developer team identifier
        * **Topic (App Bundle Id)**, the bundle ID of the app you send to

        Where to find each one: <Doc href="/docs/integrations/push/apple-push-notification">APNs integration</Doc>.
      </Step>

      <Step title="Install the SDK">
        In Xcode, go to **File > Add Packages**, paste the repository URL, pick a version, and add it to your target.

        ```text theme={null}
        https://github.com/trycourier/courier-ios
        ```

        With CocoaPods instead, add the pod and run `pod install` from your `ios/` directory. The SDK needs iOS 15.0 or later.

        ```ruby theme={null}
        platform :ios, '15.0'

        target 'YourApp' do
          pod 'Courier_iOS'
        end
        ```
      </Step>

      <Step title="Enable push in Xcode">
        Select your target, open **Signing & Capabilities**, and add **Push Notifications**.

        For silent push, add **Background Modes** as well and check **Remote notifications**.

        Optionally add a <Doc href="/docs/sdk-libraries/ios#notification-service-extension">Notification Service Extension</Doc> to track delivery while the app is closed. It is three steps in Xcode, and the iOS SDK reference walks them.
      </Step>

      <Step title="Sync the device token">
        Extend `CourierDelegate` in your `AppDelegate`. The SDK registers the APNs token, refreshes it, and forwards delivery and click events.

        ```swift theme={null}
        import Courier_iOS

        @main
        class AppDelegate: CourierDelegate {

            override func pushNotificationDeliveredInForeground(
                message: [AnyHashable: Any]
            ) -> UNNotificationPresentationOptions {
                return [.sound, .list, .banner, .badge]
            }

            override func pushNotificationClicked(message: [AnyHashable: Any]) {
                print("Notification clicked: \(message)")
            }
        }
        ```
      </Step>

      <Step title="Sign in the user">
        Courier ties every token to a signed-in user, so no push reaches the device until you call `signIn`. Pass the `user_id` you send to, plus a JWT your backend mints (see <Doc href="/docs/in-app/authenticate-users">Authenticate users</Doc>).

        <Note>
          **The `user_id` is yours, not Courier's.**<br />
          Use whatever your app already calls this user, such as a database id or your auth provider's `sub`. Nothing has to exist in Courier first, since signing in registers the user and their device tokens. With no auth yet, any stable string works, as long as you reuse it on every launch and send to that same id.
        </Note>

        ```swift theme={null}
        await Courier.shared.signIn(userId: "user_123", accessToken: jwt)
        ```

        Call it once, right after your own login resolves. Credentials persist on the device, so app launches do not need it. Call `signOut` on logout to delete that device's tokens from Courier.
      </Step>

      <Step title="Request notification permission">
        iOS shows the system dialog once. If the user denies it, they re-enable it in device Settings.

        ```swift theme={null}
        let status = try await Courier.requestNotificationPermission()
        ```

        Reading the status, and getting the user back after they deny: <Doc href="/docs/send/push/overview#notification-permissions">Notification permissions</Doc>.
      </Step>
    </Steps>

    Next: [send a test push](#send-a-test-push) to the `user_id` you signed in.
  </Tab>

  <Tab title="Android">
    <Steps>
      <Step title="Connect FCM in Courier">
        In your [Firebase project settings](https://console.firebase.google.com/), open **Service Accounts** and click **Generate new private key**.

        Then open <AppLink href="https://app.courier.com/integrations">Integrations</AppLink>, switch to the environment you send from, and choose **Firebase Cloud Messaging**. Paste the contents of the downloaded JSON into **Service Account JSON** and click **Install Provider**. Providers are per environment, so one connected in test does not exist in production.

        Leave **Apply Recommended Courier Mobile SDK Formatting** on. Full walkthrough: <Doc href="/docs/integrations/push/firebase-fcm">FCM integration</Doc>.
      </Step>

      <Step title="Install the SDK">
        Add the Jitpack repository in `settings.gradle`, then the dependency in your app's `build.gradle`. The SDK needs Android SDK 23 or later and Gradle 8.4 or later.

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

        ```gradle theme={null}
        dependencies {
            implementation 'com.github.trycourier:courier-android:LATEST_VERSION'
        }
        ```

        Take `LATEST_VERSION` from [GitHub Releases](https://github.com/trycourier/courier-android/releases). Then initialize the SDK in your `Application` class so it can persist state across sessions.

        ```kotlin theme={null}
        class YourApplication : Application() {
            override fun onCreate() {
                super.onCreate()
                Courier.initialize(this)
            }
        }
        ```
      </Step>

      <Step title="Add Firebase to your app">
        From Courier Android 6.x the SDK no longer bundles Firebase Messaging, so declare it yourself.

        ```gradle theme={null}
        dependencies {
            implementation platform('com.google.firebase:firebase-bom:33.1.2')
            implementation "com.google.firebase:firebase-messaging"
        }
        ```

        Then complete the [Firebase Android setup](https://firebase.google.com/docs/android/setup): download `google-services.json` into your app module and apply the Google Services plugin.
      </Step>

      <Step title="Sync the device token">
        Subclass `FirebaseMessagingService` and forward both callbacks to Courier. `onNewToken` is what registers and refreshes the token.

        ```kotlin theme={null}
        package your.app.package

        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 CourierPushNotificationService : FirebaseMessagingService() {

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

                val notificationIntent = CourierPushNotificationIntent(
                    context = this,
                    target = MainActivity::class.java,
                    payload = message
                )

                notificationIntent.presentNotification(
                    title = message.data["title"] ?: message.notification?.title,
                    body = message.data["body"] ?: message.notification?.body
                )

                // Call this after presenting the notification: onMessageReceived makes a
                // synchronous tracking request that would otherwise delay the notification.
                Courier.onMessageReceived(message.data)
            }

            override fun onNewToken(token: String) {
                super.onNewToken(token)
                Courier.onNewToken(token)
            }
        }
        ```

        Register the service in `AndroidManifest.xml`.

        ```xml theme={null}
        <service
            android:name=".CourierPushNotificationService"
            android:exported="false">
            <intent-filter>
                <action android:name="com.google.firebase.MESSAGING_EVENT" />
            </intent-filter>
        </service>
        ```

        Then extend `CourierActivity` in your `MainActivity` to receive delivery and click events in the foreground.

        ```kotlin theme={null}
        class MainActivity : CourierActivity() {
            override fun onPushNotificationDelivered(pushNotification: Map<String, String>) { }
            override fun onPushNotificationClicked(pushNotification: Map<String, String>) { }
        }
        ```
      </Step>

      <Step title="Sign in the user">
        Courier ties every token to a signed-in user, so no push reaches the device until you call `signIn`. Pass the `user_id` you send to, plus a JWT your backend mints (see <Doc href="/docs/in-app/authenticate-users">Authenticate users</Doc>).

        <Note>
          **The `user_id` is yours, not Courier's.**<br />
          Use whatever your app already calls this user, such as a database id or your auth provider's `sub`. Nothing has to exist in Courier first, since signing in registers the user and their device tokens. With no auth yet, any stable string works, as long as you reuse it on every launch and send to that same id.
        </Note>

        ```kotlin theme={null}
        // From an Activity or Fragment, where lifecycleScope is available
        lifecycleScope.launch {
            Courier.shared.signIn(userId = "user_123", accessToken = jwt)
        }
        ```

        Call it once, right after your own login resolves. Credentials persist on the device, so app launches do not need it. Call `signOut` on logout to delete that device's tokens from Courier.
      </Step>

      <Step title="Request notification permission">
        Android 13 and later (API 33) needs a runtime permission. Declare it in `AndroidManifest.xml`, then request it. The call is safe on older versions.

        ```xml theme={null}
        <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
        ```

        ```kotlin theme={null}
        Courier.shared.requestNotificationPermission(activity)
        ```

        Reading the status, and getting the user back after they deny: <Doc href="/docs/send/push/overview#notification-permissions">Notification permissions</Doc>.
      </Step>
    </Steps>

    Next: [send a test push](#send-a-test-push) to the `user_id` you signed in.
  </Tab>

  <Tab title="Flutter">
    <Steps>
      <Step title="Connect your push providers">
        Open <AppLink href="https://app.courier.com/integrations">Integrations</AppLink> and switch to the environment you send from. Providers are per environment, so one connected in test does not exist in production.

        * Android needs <Doc href="/docs/integrations/push/firebase-fcm">Firebase Cloud Messaging</Doc>. Generate a private key under **Service Accounts** in Firebase, paste it into **Service Account JSON**, and click **Install Provider**.
        * iOS needs <Doc href="/docs/integrations/push/apple-push-notification">APNs</Doc>. Enter your **Key Id**, `.p8` **Key**, **Team Id**, and **Topic (App Bundle Id)**, then click **Install Provider**.
      </Step>

      <Step title="Install the SDK">
        ```bash theme={null}
        flutter pub add courier_flutter
        ```

        <Warning>
          The SDK's `intl` dependency (`>=0.19.0 <1.0.0`) conflicts with Flutter 3.32+ (Dart 3.7+), which ships `intl` 1.0. On a resolution error, pin `intl: ^0.19.0` in your `pubspec.yaml`.
        </Warning>
      </Step>

      <Step title="Set up the iOS project">
        Set your deployment target to iOS 15.0 or later, then run `cd ios && pod update`.

        In Xcode, select your target, open **Signing & Capabilities**, and add **Push Notifications**. Optionally add a <Doc href="/docs/sdk-libraries/ios#notification-service-extension">Notification Service Extension</Doc> to track delivery while the app is closed.

        In `ios/Runner/AppDelegate.swift`, inherit from `CourierFlutterDelegate`. This is what syncs the APNs token.

        ```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="Set up the Android project">
        Add the Jitpack repository to `android/build.gradle`, and set `minSdkVersion 23` in `android/app/build.gradle`. Declare the Google Services plugin version in `android/settings.gradle` (`id "com.google.gms.google-services" version "4.4.2" apply false`), apply it alongside Firebase, and place `google-services.json` in `android/app/`.

        ```gradle theme={null}
        plugins {
            id "com.google.gms.google-services"
        }

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

        Extend `CourierFlutterActivity` in `MainActivity`.

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

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

        Then subclass `FirebaseMessagingService` to sync the FCM token and present notifications.

        ```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)

                val notificationIntent = CourierPushNotificationIntent(
                    context = this,
                    target = MainActivity::class.java,
                    payload = message
                )

                notificationIntent.presentNotification(
                    title = message.data["title"] ?: message.notification?.title,
                    body = message.data["body"] ?: message.notification?.body
                )

                Courier.onMessageReceived(message.data)
            }

            override fun onNewToken(token: String) {
                super.onNewToken(token)
                Courier.onNewToken(token)
            }
        }
        ```

        Register the service in `android/app/src/main/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>

      <Step title="Sign in the user">
        Courier ties every token to a signed-in user, so no push reaches the device until you call `signIn`. Pass the `user_id` you send to, plus a JWT your backend mints (see <Doc href="/docs/in-app/authenticate-users">Authenticate users</Doc>).

        <Note>
          **The `user_id` is yours, not Courier's.**<br />
          Use whatever your app already calls this user, such as a database id or your auth provider's `sub`. Nothing has to exist in Courier first, since signing in registers the user and their device tokens. With no auth yet, any stable string works, as long as you reuse it on every launch and send to that same id.
        </Note>

        ```dart theme={null}
        await Courier.shared.signIn(userId: "user_123", accessToken: jwt);
        ```

        Call it once, right after your own login resolves. Credentials persist on the device, so app launches do not need it. Call `signOut` on logout to delete that device's tokens from Courier.
      </Step>

      <Step title="Request notification permission">
        iOS shows the system dialog once. Android 13 and later needs the permission declared in `android/app/src/main/AndroidManifest.xml`, and the request is a no-op below API 33.

        ```xml theme={null}
        <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
        ```

        ```dart theme={null}
        final status = await Courier.requestNotificationPermission();
        ```

        Reading the status, and getting the user back after they deny: <Doc href="/docs/send/push/overview#notification-permissions">Notification permissions</Doc>.
      </Step>
    </Steps>

    Next: [send a test push](#send-a-test-push) to the `user_id` you signed in.
  </Tab>

  <Tab title="React Native">
    <Steps>
      <Step title="Connect your push providers">
        Open <AppLink href="https://app.courier.com/integrations">Integrations</AppLink> and switch to the environment you send from. Providers are per environment, so one connected in test does not exist in production.

        * Android needs <Doc href="/docs/integrations/push/firebase-fcm">Firebase Cloud Messaging</Doc>. Generate a private key under **Service Accounts** in Firebase, paste it into **Service Account JSON**, and click **Install Provider**.
        * iOS needs <Doc href="/docs/integrations/push/apple-push-notification">APNs</Doc>. Enter your **Key Id**, `.p8` **Key**, **Team Id**, and **Topic (App Bundle Id)**, then click **Install Provider**.
      </Step>

      <Step title="Install the SDK">
        ```bash theme={null}
        npm install @trycourier/courier-react-native
        ```
      </Step>

      <Step title="Set up the iOS project">
        Set `platform :ios, '15.0'` in your Podfile, then run `cd ios && pod install`.

        In Xcode, select your target, open **Signing & Capabilities**, and add **Push Notifications**. Optionally add a <Doc href="/docs/sdk-libraries/ios#notification-service-extension">Notification Service Extension</Doc> to track delivery while the app is closed.

        In your Swift `AppDelegate`, hold a `CourierDelegate` and forward the APNs registration callbacks to it. This is what syncs the token.

        ```swift theme={null}
        private let courierDelegate = CourierDelegate()

        func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
            courierDelegate.application(application, didRegisterForRemoteNotificationsWithDeviceToken: deviceToken)
        }

        func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) {
            courierDelegate.application(application, didFailToRegisterForRemoteNotificationsWithError: error)
        }
        ```

        <Note>
          An older app with an Objective-C `AppDelegate` inherits from `CourierReactNativeDelegate` instead: `@interface AppDelegate : CourierReactNativeDelegate`. On Expo, follow the [Expo setup guide on GitHub](https://github.com/trycourier/courier-react-native/blob/master/Docs/6_Expo.md), which wires Courier into `ExpoAppDelegate`.
        </Note>
      </Step>

      <Step title="Set up the Android project">
        Add the Jitpack repository to `android/build.gradle` and set `minSdkVersion = 23`. Add `classpath("com.google.gms:google-services:4.4.0")`, declare Firebase in `android/app/build.gradle`, and place `google-services.json` in `android/app/`.

        ```gradle theme={null}
        apply plugin: "com.google.gms.google-services"

        dependencies {
            implementation platform('com.google.firebase:firebase-bom:33.1.2')
            implementation "com.google.firebase:firebase-messaging"
        }
        ```

        Extend `CourierReactNativeActivity` in `MainActivity`.

        ```kotlin theme={null}
        import com.courierreactnative.CourierReactNativeActivity

        class MainActivity : CourierReactNativeActivity() {
            // ...
        }
        ```

        Then subclass `FirebaseMessagingService` to sync the FCM token and present notifications.

        ```java theme={null}
        package your.app.package;

        import androidx.annotation.NonNull;
        import com.courier.android.Courier;
        import com.courier.android.notifications.CourierPushNotificationIntent;
        import com.courier.android.notifications.RemoteMessageExtensionsKt;
        import com.google.firebase.messaging.FirebaseMessagingService;
        import com.google.firebase.messaging.RemoteMessage;

        public class YourNotificationService extends FirebaseMessagingService {

            @Override
            public void onMessageReceived(@NonNull RemoteMessage message) {
                super.onMessageReceived(message);

                CourierPushNotificationIntent notificationIntent = new CourierPushNotificationIntent(
                    this,
                    0,
                    MainActivity.class,
                    message
                );

                String title = message.getData().get("title");
                String body = message.getData().get("body");

                RemoteMessageExtensionsKt.presentNotification(
                    notificationIntent,
                    title,
                    body,
                    android.R.drawable.ic_dialog_info,
                    "Notification Service"
                );

                // Call this after presenting the notification: it makes a synchronous tracking request.
                Courier.Companion.onMessageReceived(message.getData());
            }

            @Override
            public void onNewToken(@NonNull String token) {
                super.onNewToken(token);
                Courier.Companion.onNewToken(token);
            }
        }
        ```

        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>

      <Step title="Sign in the user">
        Courier ties every token to a signed-in user, so no push reaches the device until you call `signIn`. Pass the `user_id` you send to, plus a JWT your backend mints (see <Doc href="/docs/in-app/authenticate-users">Authenticate users</Doc>).

        <Note>
          **The `user_id` is yours, not Courier's.**<br />
          Use whatever your app already calls this user, such as a database id or your auth provider's `sub`. Nothing has to exist in Courier first, since signing in registers the user and their device tokens. With no auth yet, any stable string works, as long as you reuse it on every launch and send to that same id.
        </Note>

        ```typescript theme={null}
        await Courier.shared.signIn({ userId: "user_123", accessToken: jwt });
        ```

        Call it once, right after your own login resolves. Credentials persist on the device, so app launches do not need it. Call `signOut` on logout to delete that device's tokens from Courier.
      </Step>

      <Step title="Request notification permission">
        iOS shows the system dialog once. Android 13 and later needs the permission declared in `AndroidManifest.xml`.

        ```xml theme={null}
        <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
        ```

        ```typescript theme={null}
        const status = await Courier.requestNotificationPermission();
        ```

        Reading the status, and getting the user back after they deny: <Doc href="/docs/send/push/overview#notification-permissions">Notification permissions</Doc>.
      </Step>
    </Steps>

    Next: [send a test push](#send-a-test-push) to the `user_id` you signed in.
  </Tab>
</Tabs>

<Info>
  The SDK holds a token it receives before anyone signs in and uploads it at sign-in, so you never race the callback. Signing in a different user signs the previous one out first.
</Info>

<Tip>
  To register tokens from a server or outside the SDK, use the <Endpoint method="PUT" path="/users/{user_id}/tokens/{token}" name="Add a token to a user" href="/docs/api-reference/device-tokens/add-a-token-to-a-user">Device Tokens API</Endpoint> directly. The <Doc href="/docs/integrations/push/overview">push notifications overview</Doc> covers the token model.
</Tip>

## Send a test push

First <Doc href="/docs/design/templates/overview">design a template</Doc> with a **Push** channel and publish it. Then send it to the `user_id` you signed in, routing to the push channel. Courier resolves the user's tokens and delivers through each connected provider.

<CodeGroup>
  ```javascript Node.js theme={null}
  import Courier from '@trycourier/courier';

  const client = new Courier({
    apiKey: process.env['COURIER_API_KEY'],
  });

  const { requestId } = await client.send.message({
    message: {
      to: { user_id: 'user_123' },
      template: 'nt_01kx4h2jdafq8bk9aftxak4b40',
      routing: { method: 'single', channels: ['push'] },
    },
  });

  console.log(requestId);
  ```

  ```python Python theme={null}
  import os
  from courier import Courier

  client = Courier(
      api_key=os.environ.get("COURIER_API_KEY"),
  )
  response = client.send.message(
      message={
          "to": {"user_id": "user_123"},
          "template": "nt_01kx4h2jdafq8bk9aftxak4b40",
          "routing": {"method": "single", "channels": ["push"]},
      },
  )
  print(response.request_id)
  ```

  ```bash cURL theme={null}
  curl -X POST https://api.courier.com/send \
    -H "Authorization: Bearer $COURIER_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{
      "message": {
        "to": { "user_id": "user_123" },
        "template": "nt_01kx4h2jdafq8bk9aftxak4b40",
        "routing": { "method": "single", "channels": ["push"] }
      }
    }'
  ```

  ```ruby Ruby theme={null}
  require "courier"

  courier = Courier::Client.new(api_key: ENV["COURIER_API_KEY"])

  response = courier.send_.message(
    message: {
      to: { user_id: "user_123" },
      template: "nt_01kx4h2jdafq8bk9aftxak4b40",
      routing: { method: "single", channels: ["push"] }
    }
  )

  puts(response)
  ```

  ```go Go theme={null}
  response, err := client.Send.Message(context.TODO(), courier.SendMessageParams{
  	Message: courier.SendMessageParamsMessage{
  		To: courier.SendMessageParamsMessageToUnion{
  			OfUserRecipient: &shared.UserRecipientParam{UserID: courier.String("user_123")},
  		},
  		Template: courier.String("nt_01kx4h2jdafq8bk9aftxak4b40"),
  		Routing: courier.SendMessageParamsMessageRouting{
  			Method:   string(shared.MessageRoutingMethodSingle),
  			Channels: []shared.MessageRoutingChannelUnionParam{{OfString: courier.String("push")}},
  		},
  	},
  })
  ```

  ```java Java theme={null}
  SendMessageParams params = SendMessageParams.builder()
      .message(SendMessageParams.Message.builder()
          .to(JsonValue.from(java.util.Map.of("user_id", "user_123")))
          .template("nt_01kx4h2jdafq8bk9aftxak4b40")
          .routing(JsonValue.from(java.util.Map.of(
              "method", "single",
              "channels", java.util.List.of("push"))))
          .build())
      .build();
  client.send().message(params);
  ```

  ```php PHP theme={null}
  $response = $client->send->message(
    message: [
      'to' => ['userID' => 'user_123'],
      'template' => 'nt_01kx4h2jdafq8bk9aftxak4b40',
      'routing' => ['method' => 'single', 'channels' => ['push']],
    ],
  );
  ```

  ```csharp C# theme={null}
  SendMessageParams parameters = new()
  {
      Message = new()
      {
          To = new UserRecipient { UserID = "user_123" },
          Template = "nt_01kx4h2jdafq8bk9aftxak4b40",
          Routing = new Routing { Method = Method.Single, Channels = ["push"] },
      },
  };

  await client.Send.Message(parameters);
  ```

  ```bash CLI theme={null}
  courier send message \
    --api-key "$COURIER_API_KEY" \
    --message '{"to":{"user_id":"user_123"},"template":"nt_01kx4h2jdafq8bk9aftxak4b40","routing":{"method":"single","channels":["push"]}}'
  ```

  ```text MCP theme={null}
  With Courier MCP, send my nt_01kx4h2jdafq8bk9aftxak4b40 template to user_123 on the push channel only.
  ```
</CodeGroup>

## Verify

The notification appears on the device you registered. For a silent payload, confirm your app handled it in the background instead. Open the message in <AppLink href="https://app.courier.com/logs">Logs</AppLink>. A delivered push shows the provider it went through and the token it targeted. If it did not deliver, the log names the reason: no token, expired token, or provider not configured.

<Note>
  `channels: ["push"]` picks a single push provider in failover order and stops at the first success. To reach both iOS and Android on one send, set the push channel's `routing_method` to `all` and put both providers on that channel. See <Doc href="/docs/send/routing#reaching-ios-and-android-in-one-send">reaching iOS and Android in one send</Doc>.
</Note>

## Delivery and click tracking

Courier tracks push delivery and clicks for you. Every push carries a `trackingUrl`, and the SDK posts back to it as the notification arrives and again when the user taps it. Both events land in <AppLink href="https://app.courier.com/logs">Logs</AppLink> with no code of your own.

The wiring from [Set up your app](#set-up-your-app) is what reports them:

| Event               | Reported by                          | Works when                         |
| :------------------ | :----------------------------------- | :--------------------------------- |
| Delivered (iOS)     | `CourierDelegate`                    | your app is in the foreground      |
| Delivered (iOS)     | the Notification Service Extension   | your app is backgrounded or closed |
| Delivered (Android) | `FirebaseMessagingService`           | any state, including killed        |
| Clicked             | `CourierDelegate`, `CourierActivity` | any state                          |

Two provider defaults keep that accurate, and a new workspace has both on. **Attach Mutable Content** on <Doc href="/docs/integrations/push/apple-push-notification">APNs</Doc> is what lets the extension run at all. **Apply Recommended Courier Mobile SDK Formatting** on <Doc href="/docs/integrations/push/firebase-fcm">FCM</Doc> ships the push as `data`, so Android wakes for it. <Doc href="/docs/send/push/custom-data#how-the-data-arrives-per-provider">How the data arrives per provider</Doc> shows both payloads.

<Note>
  If you handle notifications outside the Courier hooks, post delivery and click events yourself with `client.tracking.postTrackingUrl`. Each SDK page carries the call: <Doc href="/docs/sdk-libraries/ios#url-tracking">iOS</Doc>, <Doc href="/docs/sdk-libraries/android#url-tracking">Android</Doc>, <Doc href="/docs/sdk-libraries/flutter#url-tracking">Flutter</Doc>, and <Doc href="/docs/sdk-libraries/react-native#url-tracking">React Native</Doc>.
</Note>
