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

# Authenticate users with a JWT

> Mint a scoped JWT on your backend and sign the user in to any Courier client SDK.

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

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

<Info>
  This is the auth model for every Courier client SDK. One sign-in covers Inbox, Toasts, the preference center, and mobile push tokens.
</Info>

Courier's client SDKs authenticate with a **JWT** (JSON Web Token). Your backend mints a short-lived, scoped token with your Courier API key, and the SDK signs the user in with it. Inbox and Toasts read the same scopes. The preference center and mobile push each add their own. <Doc href="/docs/in-app/add-an-inbox">Add an inbox</Doc> covers embedding the components first.

## How the JWT flow works

The private API key that signs a JWT stays on your server, so your backend mints every token, never client code:

<Steps>
  <Step title="Your app calls your backend">
    When a user signs in, your app requests a token from your backend (for example, `GET /api/courier-jwt`).
  </Step>

  <Step title="Your backend calls Courier">
    Your backend uses your <AppLink href="https://app.courier.com/settings/api-keys">workspace API key</AppLink> to call the <Endpoint method="POST" path="/auth/issue-token" name="Create a JWT" href="/docs/api-reference/authentication/create-a-jwt">Issue Token endpoint</Endpoint> with the scopes the user needs. Keys are per <Doc href="/docs/workspaces/overview#environments-and-api-keys">environment</Doc>. Use the one matching the environment you're issuing tokens for.
  </Step>

  <Step title="Your backend returns the JWT to the client">
    The client passes the token to the SDK through `signIn({ userId, jwt })`.
  </Step>
</Steps>

## 1. Issue a token

Call <Endpoint method="POST" path="/auth/issue-token" name="Create a JWT" href="/docs/api-reference/authentication/create-a-jwt" /> from your backend with your API key and a scope string:

<CodeGroup>
  ```javascript Node.js theme={null}
  const { token } = await client.auth.issueToken({
    scope: "user_id:user_123 inbox:read:messages inbox:write:events",
    expires_in: "2 days",
  });
  ```

  ```python Python theme={null}
  response = client.auth.issue_token(
      scope="user_id:user_123 inbox:read:messages inbox:write:events",
      expires_in="2 days",
  )
  ```

  ```bash cURL theme={null}
  curl --request POST \
    --url https://api.courier.com/auth/issue-token \
    --header "Authorization: Bearer $COURIER_API_KEY" \
    --header 'Content-Type: application/json' \
    --data '{
      "scope": "user_id:user_123 inbox:read:messages inbox:write:events",
      "expires_in": "2 days"
    }'
  ```

  ```ruby Ruby theme={null}
  response = courier.auth.issue_token(
    scope: "user_id:user_123 inbox:read:messages inbox:write:events",
    expires_in: "2 days"
  )
  ```

  ```go Go theme={null}
  response, err := client.Auth.IssueToken(context.TODO(), courier.AuthIssueTokenParams{
    Scope:     "user_id:user_123 inbox:read:messages inbox:write:events",
    ExpiresIn: "2 days",
  })
  ```

  ```java Java theme={null}
  AuthIssueTokenParams params = AuthIssueTokenParams.builder()
      .scope("user_id:user_123 inbox:read:messages inbox:write:events")
      .expiresIn("2 days")
      .build();

  var response = client.auth().issueToken(params);
  ```

  ```php PHP theme={null}
  $response = $client->auth->issueToken(
      scope: 'user_id:user_123 inbox:read:messages inbox:write:events',
      expiresIn: '2 days',
  );
  ```

  ```csharp C# theme={null}
  var response = await client.Auth.IssueToken(new AuthIssueTokenParams
  {
      Scope = "user_id:user_123 inbox:read:messages inbox:write:events",
      ExpiresIn = "2 days"
  });
  ```

  ```bash CLI theme={null}
  courier auth issue-token \
    --scope "user_id:user_123 inbox:read:messages inbox:write:events" \
    --expires-in "2 days"
  ```

  ```text MCP theme={null}
  With Courier MCP, issue a two-day inbox token for user_123.
  ```
</CodeGroup>

### Scopes

`scope` is one space-separated string. Copy the line for what you are building, and swap in your user's id.

<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. The same id goes in two places, `user_id:` in the scope and the `userId` you pass to `signIn`, and they have to match.
</Note>

```text Inbox and Toasts theme={null}
user_id:user_123 inbox:read:messages inbox:write:events
```

```text Add the preference center theme={null}
user_id:user_123 inbox:read:messages inbox:write:events read:preferences write:preferences
```

```text Add mobile push theme={null}
user_id:user_123 inbox:read:messages inbox:write:events write:user-tokens
```

```text Everything theme={null}
user_id:user_123 inbox:read:messages inbox:write:events read:preferences write:preferences write:user-tokens read:brands
```

Every string starts with `user_id:` and the two inbox scopes. What each one grants:

| Scope                                  | Grants                                                                               |
| :------------------------------------- | :----------------------------------------------------------------------------------- |
| `user_id:user_123`                     | Binds the token to one user. Always required.                                        |
| `inbox:read:messages`                  | Read the user's inbox messages.                                                      |
| `inbox:write:events`                   | Mark messages read, opened, archived.                                                |
| `read:preferences` `write:preferences` | The embedded <Doc href="/docs/in-app/add-a-preference-center">preference center</Doc>.    |
| `write:user-tokens`                    | Sync <Doc href="/docs/integrations/push/overview">push tokens</Doc> from a mobile client. |
| `read:brands`                          | Apply a <Doc href="/docs/design/brands">brand</Doc> to the rendered inbox.                |

For every scope value, see the <Endpoint method="POST" path="/auth/issue-token" name="Create a JWT" href="/docs/api-reference/authentication/create-a-jwt#body-scope">`scope` parameter in the Issue Token API reference</Endpoint>.

## 2. Sign in on the client

Signing in tells the SDK which user it is fetching messages for. Until it runs, the components render empty.

Pass the JWT to `signIn` when your app starts or the user logs in:

<CodeGroup>
  ```jsx React highlight={9} theme={null}
  import { useEffect } from "react";
  import { useCourier } from "@trycourier/courier-react";  // or "@trycourier/courier-react-17"

  export default function App() {
    const courier = useCourier();

    useEffect(() => {
      // jwt is the token your backend issued in step 1
      courier.shared.signIn({ userId, jwt });
    }, []);

    // ... render <CourierInbox /> or <CourierToast />
  }
  ```

  ```html Web Components highlight={5} theme={null}
  <script type="module">
    import { Courier } from "@trycourier/courier-ui-inbox";

    // jwt is the token your backend issued in step 1
    Courier.shared.signIn({ userId, jwt });
  </script>
  ```

  ```vue Vue highlight={9} theme={null}
  <script setup lang="ts">
  import { onMounted } from "vue";
  import { useCourier } from "@trycourier/courier-vue";

  const courier = useCourier();

  onMounted(() => {
    // jwt is the token your backend issued in step 1
    courier.shared.signIn({ userId, jwt });
  });
  </script>
  ```

  ```ts Angular highlight={10} theme={null}
  import { Component, OnInit, inject } from "@angular/core";
  import { CourierService } from "@trycourier/courier-angular";

  @Component({ selector: "app-root", standalone: true, template: `...` })
  export class AppComponent implements OnInit {
    private readonly courier = inject(CourierService);

    ngOnInit(): void {
      // jwt is the token your backend issued in step 1
      this.courier.signIn({ userId, jwt });
    }
  }
  ```

  ```swift iOS highlight={5} theme={null}
  import Courier_iOS

  // jwt is the token your backend issued in step 1
  func authenticate() async {
    await Courier.shared.signIn(userId: userId, accessToken: jwt)
  }
  ```

  ```kotlin Android highlight={1,4} theme={null}
  // signIn is a suspend function; call it from a coroutine
  lifecycleScope.launch {
    // jwt is the token your backend issued in step 1
    Courier.shared.signIn(userId = userId, accessToken = jwt)
  }
  ```

  ```dart Flutter highlight={3} theme={null}
  // jwt is the token your backend issued in step 1
  Future<void> authenticate() async {
    await Courier.shared.signIn(accessToken: jwt, userId: userId);
  }
  ```

  ```jsx React Native highlight={7} theme={null}
  import { useEffect } from "react";
  import Courier from "@trycourier/courier-react-native";

  export default function App() {
    useEffect(() => {
      // jwt is the token your backend issued in step 1
      Courier.shared.signIn({ accessToken: jwt, userId });
    }, []);

    // ... render <CourierInboxView />
  }
  ```
</CodeGroup>

## 3. Read the signed-in user

Every platform exposes the current user and a listener that fires on sign-in and sign-out. Use the listener to clear app state on sign-out, or to re-render once credentials are restored on launch.

<CodeGroup>
  ```jsx React theme={null}
  import { useCourier } from "@trycourier/courier-react";

  const { auth } = useCourier();
  auth.userId;  // undefined when signed out
  ```

  ```html Web Components theme={null}
  <script type="module">
    import { Courier } from "@trycourier/courier-ui-inbox";

    const listener = Courier.shared.addAuthenticationListener(({ userId }) => {
      console.log(userId ?? "No user signed in");
    });

    listener.remove();
  </script>
  ```

  ```vue Vue theme={null}
  <script setup lang="ts">
  import { useCourier } from "@trycourier/courier-vue";

  const courier = useCourier();
  courier.auth.value.userId;  // undefined when signed out
  </script>
  ```

  ```ts Angular theme={null}
  import { CourierService } from "@trycourier/courier-angular";

  // auth$ emits on every sign-in and sign-out
  this.courier.auth$.subscribe(({ userId }) => {
    console.log(userId ?? "No user signed in");
  });
  ```

  ```swift iOS theme={null}
  Task {
    let userId = await Courier.shared.userId
    let isSignedIn = await Courier.shared.isUserSignedIn

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

    listener.remove()
  }
  ```

  ```kotlin Android theme={null}
  val userId = Courier.shared.userId
  val isSignedIn = Courier.shared.isUserSignedIn

  val listener = Courier.shared.addAuthenticationListener { userId ->
    print(userId ?: "No user signed in")
  }

  listener.remove()
  ```

  ```dart Flutter 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();
  ```

  ```jsx React Native theme={null}
  const userId = await Courier.shared.getUserId();
  const tenantId = await Courier.shared.getTenantId();
  const isSignedIn = await Courier.shared.isUserSignedIn();

  const listener = await Courier.shared.addAuthenticationListener({
    onUserChanged: (userId) => console.log("User changed:", userId),
  });

  await listener.remove();
  ```
</CodeGroup>

<Note>
  On mobile, credentials **persist across app sessions**. The SDK restores the last signed-in user on launch, so no cold start needs `signIn`. Call it again only when the user changes or the token expires.
</Note>

## 4. Sign out

Call `signOut` when the user logs out of your app. It clears stored credentials, disconnects the real-time socket, and on mobile stops associating push tokens with that user.

<CodeGroup>
  ```jsx React theme={null}
  const { auth } = useCourier();
  auth.signOut();
  ```

  ```js Web Components theme={null}
  Courier.shared.signOut();
  ```

  ```vue Vue theme={null}
  const courier = useCourier();
  courier.auth.value.signOut();
  ```

  ```ts Angular theme={null}
  this.courier.signOut();
  ```

  ```swift iOS theme={null}
  Task {
    await Courier.shared.signOut()
  }
  ```

  ```kotlin Android theme={null}
  lifecycleScope.launch {
    Courier.shared.signOut()
  }
  ```

  ```dart Flutter theme={null}
  await Courier.shared.signOut();
  ```

  ```jsx React Native theme={null}
  await Courier.shared.signOut();
  ```
</CodeGroup>

## 5. Refresh tokens

Set `expires_in` to match your session length. A short-lived token can expire while a tab stays open, which silently empties the inbox.

The SDKs do **not** refresh tokens. Before the current token expires, mint a new JWT on your backend and call `signIn` again with it.

<Warning>
  `expires_in` is optional, and omitting it mints a token that **never expires**. Always set it. A leaked token with no `exp` grants that user's inbox forever. The only fix is rotating the API key that signed it.
</Warning>

## EU-hosted workspaces

Skip this section unless your workspace is on the <Doc href="/docs/workspaces/security#regions-and-data-residency">EU datacenter</Doc>. Every client SDK defaults to Courier's **US** hosts.

Pass regional URLs through `apiUrls` on `signIn`:

<CodeGroup>
  ```jsx React highlight={6} theme={null}
  import { getCourierApiUrlsForRegion } from "@trycourier/courier-react";

  courier.shared.signIn({
    userId,
    jwt,
    apiUrls: getCourierApiUrlsForRegion("eu"),
  });
  ```

  ```js Web Components highlight={6} theme={null}
  import { Courier, EU_COURIER_API_URLS } from "@trycourier/courier-ui-inbox";

  Courier.shared.signIn({
    userId,
    jwt,
    apiUrls: EU_COURIER_API_URLS,
  });
  ```

  ```vue Vue highlight={6} theme={null}
  import { getCourierApiUrlsForRegion } from "@trycourier/courier-vue";

  courier.shared.signIn({
    userId,
    jwt,
    apiUrls: getCourierApiUrlsForRegion("eu"),
  });
  ```

  ```ts Angular highlight={6} theme={null}
  import { getCourierApiUrlsForRegion } from "@trycourier/courier-angular";

  this.courier.signIn({
    userId,
    jwt,
    apiUrls: getCourierApiUrlsForRegion("eu"),
  });
  ```

  ```swift iOS highlight={4} theme={null}
  await Courier.shared.signIn(
    userId: userId,
    accessToken: jwt,
    apiUrls: .eu
  )
  ```

  ```kotlin Android highlight={4} theme={null}
  Courier.shared.signIn(
    accessToken = jwt,
    userId = userId,
    apiUrls = CourierClient.ApiUrls.eu()
  )
  ```

  ```dart Flutter highlight={4} theme={null}
  await Courier.shared.signIn(
    accessToken: jwt,
    userId: userId,
    apiUrls: CourierApiUrls.eu,
  );
  ```

  ```jsx React Native highlight={6} theme={null}
  import { getCourierApiUrlsForRegion } from "@trycourier/courier-react-native";

  await Courier.shared.signIn({
    userId,
    accessToken: jwt,
    apiUrls: getCourierApiUrlsForRegion("eu"),
  });
  ```
</CodeGroup>

The web packages re-export three helpers from `@trycourier/courier-js`:

| Helper                               | Returns                                                                                   |
| :----------------------------------- | :---------------------------------------------------------------------------------------- |
| `EU_COURIER_API_URLS`                | Frozen EU preset (`api.eu.courier.com`, `inbox.eu.courier.io`, `realtime.eu.courier.io`). |
| `DEFAULT_COURIER_API_URLS`           | Frozen US preset, the default.                                                            |
| `getCourierApiUrlsForRegion(region)` | A mutable **copy** for `"us"` or `"eu"`, so you can override a single host.               |

<Warning>
  Mint the JWT against the **same region** as the client. For EU, your backend must call `https://api.eu.courier.com/auth/issue-token`. A US-issued token will not authenticate against EU hosts.
</Warning>

For the `CourierApiUrls` type shape, see <Doc href="/docs/sdk-libraries/courier-js-web#courierapiurls">Courier JS models</Doc>.

## Troubleshooting

**The inbox loads but shows no messages.** The SDK connects and throws nothing, so check these four causes in order. Decode the JWT at [jwt.io](https://www.jwt.io) for the first three:

1. **`exp` is in the future.** A token that expired between page loads signs in without error and returns nothing.
2. **`scope` includes `inbox:read:messages` and `inbox:write:events`.** Without them the connection succeeds and the feed is empty.
3. **The `user_id:` in the scope matches the `userId` you pass to `signIn`.** A mismatch reads an empty inbox belonging to whoever the token names.
4. **The `tenantId` matches the send.** A `tenantId` filters the feed, and the unread count, to that tenant. A message sent without a tenant, or in a different one, will not appear. Signing in without a `tenantId` applies no filter. See <Doc href="/docs/in-app/send-to-the-inbox#scope-the-inbox-to-a-tenant">scope the inbox to a tenant</Doc>.

**Preferences editor is empty or read-only.** Add `read:preferences write:preferences` to the scope string.

**Push tokens never appear on the user.** Add `write:user-tokens` to the scope string. <Guide href="/docs/guides/set-up-mobile-push#set-up-your-app">Set up push notifications</Guide> covers the registration that follows.

**Everything 401s on an EU workspace.** The token and the client must be in the same region. Confirm your backend mints against `https://api.eu.courier.com/auth/issue-token` and the client passes EU [`apiUrls`](#eu-hosted-workspaces).

## FAQ

<AccordionGroup>
  <Accordion title="Why does the inbox connect but show no messages?">
    The token signed in but lacks read access. Confirm the [scope string](#scopes) includes `inbox:read:messages` and `inbox:write:events`, and that `exp` has not passed.
  </Accordion>

  <Accordion title="Do Inbox and Toasts need separate tokens?">
    They read the same feed, so one `signIn` with the same JWT and scopes covers both.
  </Accordion>

  <Accordion title="I'm on an older inbox SDK with a different auth setup.">
    Older SDK versions used a different authentication method. To move to JWT, see the <Doc href="/docs/sdk-libraries/courier-react-v8-migration-guide">courier-react v8 migration guide</Doc>.
  </Accordion>
</AccordionGroup>
