> ## 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`, the default import of the v7 Node SDK. 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.

# Push notifications

> How device tokens are stored on the user, synced by the SDK, and used to send push.

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>;
};

A push reaches a device through a token, and that token belongs to a signed-in user.

The Courier mobile SDKs register and refresh those tokens for you. How much they do depends on the native hook you wired up, so it is per provider, not per platform.

| Provider                 | Key            | iOS                             | Android                                  |
| :----------------------- | :------------- | :------------------------------ | :--------------------------------------- |
| APNs                     | `apn`          | Automatic via `CourierDelegate` | Not applicable                           |
| Firebase Cloud Messaging | `firebase-fcm` | Manual                          | Automatic via `FirebaseMessagingService` |
| Expo                     | `expo`         | Manual                          | Manual                                   |
| OneSignal                | `onesignal`    | Manual                          | Manual                                   |
| Pusher Beams             | `pusher-beams` | Manual                          | Manual                                   |

Anything marked Manual you register yourself, and the rest of this page is how.

## Hand the SDK a token

`setToken` is for a provider the SDK cannot fetch from, one marked Manual in the table above. You supply the string, and everything after that is still the SDK's job.

**It writes to the same place the API does.** `setToken` calls <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" /> against the signed-in user, with the device record attached. There is no separate SDK store.

It first deletes whichever token it last cached for that provider, so a device does not leave stale rows behind. That delete is best effort, and a failure is logged rather than thrown.

Call it before anyone signs in and the token is only held on the device. `signIn` writes every held token to that user, and `signOut` deletes them again, so a shared device stops receiving the previous user's pushes.

<Note>
  The device rotates its token on reinstall, on restore, and when app data is cleared. Call `setToken` again from that provider's own refresh callback, or the stale one fails at the provider rather than in your code.
</Note>

<CodeGroup>
  ```swift iOS theme={null}
  Task {
      // APNs token
      try await Courier.shared.setAPNSToken(deviceToken)

      // FCM or another provider
      try await Courier.shared.setToken(for: .firebaseFcm, token: fcmToken)
  }
  ```

  ```kotlin Android theme={null}
  lifecycleScope.launch {
      Courier.shared.setToken(
          provider = "firebase-fcm",
          token = "your_messaging_token"
      )
  }
  ```

  ```dart Flutter theme={null}
  await Courier.shared.setTokenForProvider(
    token: fcmToken,
    provider: CourierPushProvider.firebaseFcm,
  );

  await Courier.shared.setToken(token: 'token_value', provider: 'YOUR_PROVIDER');
  ```

  ```typescript React Native theme={null}
  import { CourierPushProvider } from "@trycourier/courier-react-native";

  await Courier.shared.setTokenForProvider({
      provider: CourierPushProvider.FIREBASE_FCM,
      token: "your_messaging_token"
  });

  await Courier.shared.setToken({ key: "your-provider-key", token: "your_messaging_token" });
  ```
</CodeGroup>

## Manage tokens yourself

`setToken` still leaves the lifecycle to the SDK. To do the CRUD yourself, address the same user through one of two APIs.

From a backend, or an app with no Courier SDK, use the Token Management API:

* Store one with <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" />
* Read what a user has with <Endpoint method="GET" path="/users/{user_id}/tokens" name="List tokens" href="/docs/api-reference/device-tokens/list-tokens" />
* Remove one on logout with <Endpoint method="DELETE" path="/users/{user_id}/tokens/{token}" name="Delete user token" href="/docs/api-reference/device-tokens/delete-user-token" />

Calling all three is then your job, on every device. Nothing cleans up on sign-out, and nothing re-registers a rotated token.

From inside the app, without signing anyone in, construct a `CourierClient` and go through its token client. It reaches the same two endpoints, authenticating with the JWT the way `signIn` does, and it writes nothing to `Courier.shared`.

<CodeGroup>
  ```swift iOS theme={null}
  let client = CourierClient(jwt: jwt, userId: "user_123")

  try await client.tokens.putUserToken(
      token: fcmToken,
      provider: "firebase-fcm"
  )

  try await client.tokens.deleteUserToken(token: fcmToken)
  ```

  ```kotlin Android theme={null}
  val client = CourierClient(jwt = jwt, userId = "user_123")

  client.tokens.putUserToken(
      token = fcmToken,
      provider = "firebase-fcm",
      device = CourierDevice.current(context)
  )

  client.tokens.deleteUserToken(token = fcmToken)
  ```

  ```dart Flutter theme={null}
  final client = CourierClient(jwt: jwt, userId: 'user_123');

  await client.tokens.putUserToken(
    token: fcmToken,
    provider: 'firebase-fcm',
  );

  await client.tokens.deleteUserToken(token: fcmToken);
  ```

  ```typescript React Native theme={null}
  const client = new CourierClient({ userId: "user_123", jwt: jwt });

  await client.tokens.putUserToken({
    token: fcmToken,
    provider: "firebase-fcm",
  });

  await client.tokens.deleteUserToken({ token: fcmToken });
  ```
</CodeGroup>

<Note>
  The client covers writing and deleting only. Listing a user's tokens is REST, so <Endpoint method="GET" path="/users/{user_id}/tokens" name="List tokens" href="/docs/api-reference/device-tokens/list-tokens" /> has no client equivalent. On Android `device` is required, and `CourierDevice.current(context)` fills it.
</Note>

## Read the tokens the SDK is holding

Useful when a push does not arrive and you need to confirm the device actually registered.

<CodeGroup>
  ```swift iOS theme={null}
  let apns = await Courier.shared.apnsToken            // Data?
  let fcm = await Courier.shared.getToken(for: .firebaseFcm)
  let all = await Courier.shared.tokens                // [String: String]
  ```

  ```kotlin Android theme={null}
  val fcm = Courier.shared.fcmToken

  // Force Firebase to hand the SDK a fresh token
  lifecycleScope.launch {
      Courier.shared.refreshFcmToken()
      val token = Courier.shared.getToken(provider = "your-provider-key")
  }
  ```

  ```dart Flutter theme={null}
  final fcm = await Courier.shared.getTokenForProvider(
    provider: CourierPushProvider.firebaseFcm,
  );
  final other = await Courier.shared.getToken(provider: 'YOUR_PROVIDER');
  ```

  ```typescript React Native theme={null}
  const forKey = await Courier.shared.getToken({ key: "your-provider-key" });
  const forProvider = await Courier.shared.getTokenForProvider({
    provider: CourierPushProvider.EXPO,
  });
  const all = await Courier.shared.getAllTokens();
  ```
</CodeGroup>

## Notification permissions

Each SDK wraps the platform permission calls, so you prompt, read the status, and recover from a denial without touching the native APIs.

| Function                          | Does                                                                            |
| :-------------------------------- | :------------------------------------------------------------------------------ |
| `requestNotificationPermission`   | Shows the system dialog. The setup steps above call this once.                  |
| `getNotificationPermissionStatus` | Reads the status without prompting. Android calls it `isPushPermissionGranted`. |
| `openSettingsForApp`              | Opens your app's settings page, the only route back after a denial.             |

<CodeGroup>
  ```swift iOS theme={null}
  // Prompts once, then returns the resulting UNAuthorizationStatus
  let status = try await Courier.requestNotificationPermission()

  // Reads the status without prompting
  let current = try await Courier.getNotificationPermissionStatus()

  // Send the user here after they deny the dialog
  Courier.openSettingsForApp()
  ```

  ```kotlin Android theme={null}
  // Prompts on Android 13 and later, a no-op below API 33
  Courier.shared.requestNotificationPermission(activity)

  // Reads the status without prompting
  val isGranted = Courier.shared.isPushPermissionGranted(context)
  ```

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

  final current = await Courier.getNotificationPermissionStatus();

  await Courier.openSettingsApp();
  ```

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

  const current = await Courier.getNotificationPermissionStatus();

  Courier.openSettingsForApp();
  ```
</CodeGroup>

The system dialog appears once. After that only the user can change the setting, so read the status on launch and offer your own prompt into settings when it is denied.

<Note>
  Android has no `openSettingsForApp`, and Flutter names it `openSettingsApp`. iOS returns a `UNAuthorizationStatus`, Flutter and React Native return a string, and Android's check returns a `Boolean`.
</Note>

## Control how iOS presents a foreground push

By default iOS suppresses the system banner while your app is open. Set the presentation options to change that.

<CodeGroup>
  ```swift iOS theme={null}
  // Returned from your CourierDelegate override
  override func pushNotificationDeliveredInForeground(
      message: [AnyHashable: Any]
  ) -> UNNotificationPresentationOptions {
      return [.sound, .list, .banner, .badge]
  }
  ```

  ```dart Flutter theme={null}
  Courier.setIOSForegroundPresentationOptions(options: [
    iOSNotificationPresentationOption.banner,
    iOSNotificationPresentationOption.sound,
    iOSNotificationPresentationOption.list,
    iOSNotificationPresentationOption.badge,
  ]);
  ```

  ```typescript React Native theme={null}
  Courier.setIOSForegroundPresentationOptions({
    options: ["sound", "badge", "list", "banner"],
  });
  ```
</CodeGroup>
