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

> Embed an in-app inbox, toasts, and preferences center in Vue 3 apps. Use the useCourier composable for programmatic control and real-time updates.

<img src="https://mintcdn.com/courier-4f1f25dc/dzArjiynRzFLC9wE/assets/sdks/courier-inbox-vue-banner.png?fit=max&auto=format&n=dzArjiynRzFLC9wE&q=85&s=18a98d36691cbc2d9d58d0c1298dfa2c" alt="Courier Vue Inbox" width="2080" height="1000" data-path="assets/sdks/courier-inbox-vue-banner.png" />

The Courier Vue SDK provides ready-made components and a programmatic composable for building notification experiences:

* `<CourierInbox />` — full-featured inbox for displaying and managing messages
* `<CourierInboxPopupMenu />` — popup menu version of the inbox
* `<CourierToast />` — toast notifications for time-sensitive alerts
* `<CourierPreferences />` — notification preferences center for managing topic subscriptions and delivery
* `useCourier()` — composable for programmatic access and custom UIs

<Tip>
  See these components in action with the [interactive Inbox demo](https://www.courier.com/inbox-demo) — no setup required.
</Tip>

## Installation

Available on
<Link href="https://github.com/trycourier/courier-web/tree/main/%40trycourier/courier-vue"><Icon icon="github" iconType="solid" /> GitHub</Link>
and <Link href="https://www.npmjs.com/package/@trycourier/courier-vue"><Icon icon="npm" iconType="solid" /> npm</Link>.

```bash theme={null}
npm install @trycourier/courier-vue
```

`vue` (>= 3.3) is a peer dependency.

<Note>
  Not using Vue? Check out the
  [@trycourier/courier-ui-inbox](/docs/sdk-libraries/courier-ui-inbox-web/) and
  [@trycourier/courier-ui-toast](/docs/sdk-libraries/courier-ui-inbox-web#toast-web-components)
  packages instead, which provide Web Components for any JavaScript project.
</Note>

## Authentication

To use the SDK, you need to generate a JWT (JSON Web Token) for your user. **This JWT should always be generated by your backend server, never in client-side code.**

<Steps>
  <Step title="Your client calls your backend">
    When your app needs to authenticate a user, your client
    should make a request to your own backend (ex. `GET https://your-awesome-app.com/api/generate-courier-jwt`).
  </Step>

  <Step title="Your backend calls Courier">
    In your backend endpoint, use your [Courier API Key](/docs/platform/workspaces/environments-api-keys) to call the [Courier Issue Token Endpoint](/docs/api-reference/authentication/create-a-jwt) and generate a JWT for the user.
  </Step>

  <Step title="Your backend returns the JWT to your client">
    Having received the JWT from Courier, your backend should return it to your client and pass it to the Courier SDK.
  </Step>
</Steps>

<Note>
  For a step-by-step walkthrough of authentication and token generation, see our <Link href="/docs/tutorials/inbox/how-to-send-jwt"> JWT authentication tutorial</Link>.
</Note>

### Development testing with cURL

To quickly test JWT generation for development only, you can call the [Issue Token Endpoint](/docs/api-reference/authentication/create-a-jwt) directly.

<Warning>
  Do not call the Issue Token API from client-side code. Always keep your Courier API keys secure.
</Warning>

```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 inbox:read:messages inbox:write:events",
    "expires_in": "1 day"
  }'
```

## Quick Start

Get up and running with Courier Vue in minutes. This minimal example shows how to add the inbox component to your app. Courier components are normal Vue components — import them and use them in your template.

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

const courier = useCourier();

onMounted(() => {
  // Generate a JWT for your user on your backend server
  const jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...";

  // Authenticate the user
  courier.shared.signIn({
    userId: "$YOUR_USER_ID",
    jwt: jwt,
  });
});
</script>

<template>
  <CourierInbox />
</template>
```

<Tip>
  Follow the [inbox implementation tutorial](/docs/tutorials/inbox/how-to-implement-inbox) for detailed step-by-step guidance including backend JWT generation.
</Tip>

## Inbox Component

### `<CourierInbox />`

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

const courier = useCourier();

onMounted(() => {
  const jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...";
  courier.shared.signIn({ userId: "$YOUR_USER_ID", jwt });
});
</script>

<template>
  <CourierInbox />
</template>
```

<Note>
  If you're using [tenants](/docs/platform/tenants/tenants-overview), scope requests to a particular
  tenant by passing its ID to `signIn`:

  ```js theme={null}
  courier.shared.signIn({ userId: "my-user-id", jwt, tenantId: "my-tenant-id" });
  ```

  For the full reference of sign in parameters, see the [Courier JS docs](/docs/sdk-libraries/courier-js-web#courierclient-options).
</Note>

### `<CourierInboxPopupMenu />`

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

const courier = useCourier();

onMounted(() => {
  const jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...";
  courier.shared.signIn({ userId: "$YOUR_USER_ID", jwt });
});
</script>

<template>
  <div style="padding: 24px">
    <CourierInboxPopupMenu />
  </div>
</template>
```

## Tabs and Feeds

Tabs and feeds organize and filter messages in the inbox. A **feed** is a container that groups related **tabs** together. Each tab applies filters to show relevant messages. Pass them with the `:feeds` prop.

<Note>
  If there is only one feed, the feed selection dropdown is hidden. If a feed has only one tab, the tab bar is hidden and the unread count appears next to the feed.
</Note>

Filter options for each tab:

| Filter Property | Type                 | Description                                                         |
| :-------------- | :------------------- | :------------------------------------------------------------------ |
| `tags`          | `string[]`           | Messages that have any of the specified tags                        |
| `archived`      | `boolean`            | Whether to include archived messages (defaults to `false` if unset) |
| `status`        | `'read' \| 'unread'` | Filter by read/unread status                                        |

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

const feeds: CourierInboxFeed[] = [
  {
    feedId: "notifications",
    title: "Notifications",
    tabs: [
      { datasetId: "all-notifications", title: "All", filter: {} },
      { datasetId: "unread-notifications", title: "Unread", filter: { status: "unread" } },
      { datasetId: "important", title: "Important", filter: { tags: ["important"] } },
      { datasetId: "archived", title: "Archived", filter: { archived: true } },
    ],
  },
];
</script>

<template>
  <CourierInbox :feeds="feeds" />
</template>
```

### Handle Clicks and Presses

Listen for interactions with `@message-click`, `@message-action-click`, and `@message-long-press`.

| Event                   | Payload                                  |
| :---------------------- | :--------------------------------------- |
| `@message-click`        | `CourierInboxListItemFactoryProps`       |
| `@message-action-click` | `CourierInboxListItemActionFactoryProps` |
| `@message-long-press`   | `CourierInboxListItemFactoryProps`       |

<Tip>
  `message-long-press` is only applicable on devices that support touch events.
</Tip>

```vue theme={null}
<script setup lang="ts">
import { CourierInbox } from "@trycourier/courier-vue";
import type {
  CourierInboxListItemFactoryProps,
  CourierInboxListItemActionFactoryProps,
} from "@trycourier/courier-vue";

function onMessageClick({ message, index }: CourierInboxListItemFactoryProps) {
  alert(`Message clicked at index ${index}:\n${JSON.stringify(message, null, 2)}`);
}
function onMessageActionClick({ action }: CourierInboxListItemActionFactoryProps) {
  alert(`Action clicked: ${JSON.stringify(action, null, 2)}`);
}
</script>

<template>
  <CourierInbox :on-message-click="onMessageClick" :on-message-action-click="onMessageActionClick" />
</template>
```

### Styles and Theming

Customize the inbox to match your app with a theme object passed to `:light-theme` and/or `:dark-theme`. Pass the object directly — the component serializes it for you.

```vue theme={null}
<script setup lang="ts">
import { CourierInbox, type CourierInboxTheme } from "@trycourier/courier-vue";

const theme: CourierInboxTheme = {
  inbox: {
    header: { filters: { unreadIndicator: { backgroundColor: "#8B5CF6" } } },
    list: { item: { unreadIndicatorColor: "#8B5CF6" } },
  },
};
</script>

<template>
  <CourierInbox :light-theme="theme" :dark-theme="theme" mode="light" />
</template>
```

Theme utilities: `defaultLightTheme` / `defaultDarkTheme` provide the default inbox themes, and `mergeTheme(baseTheme, overrideTheme)` merges two themes with the override taking precedence.

The full `CourierInboxTheme` type covers the popup trigger button, the inbox window (header, feeds, tabs, actions), the message list (items, scrollbar, menus), and loading/empty/error states. Every property is optional. See the complete reference in the [Courier React SDK theme reference](/docs/sdk-libraries/courier-react-web#styles-and-theming) (the theme object is identical across SDKs).

### Popup alignment and dimensions

| Vertical Alignment | Options                                              |
| :----------------- | :--------------------------------------------------- |
| Top                | `"top-right"`, `"top-center"`, `"top-left"`          |
| Center             | `"center-right"`, `"center-center"`, `"center-left"` |
| Bottom             | `"bottom-right"`, `"bottom-center"`, `"bottom-left"` |

```vue theme={null}
<CourierInboxPopupMenu
  popup-alignment="top-left"
  popup-width="340px"
  popup-height="400px"
  top="44px"
  left="44px"
/>
```

#### Fixed height

`<CourierInbox />` has a default height of `auto`. Set a fixed height with the `height` prop:

```vue theme={null}
<CourierInbox height="50vh" />
```

### Custom Elements

You can customize individual parts of the inbox by passing **render props** — functions that receive typed props and return a Vue node built with `h()`. Bind them with `:render-*`.

| Render Prop               | Type Signature                                                             |
| :------------------------ | :------------------------------------------------------------------------- |
| `:render-list-item`       | `(props: CourierInboxListItemFactoryProps) => VNodeChild`                  |
| `:render-header`          | `(props: CourierInboxHeaderFactoryProps) => VNodeChild`                    |
| `:render-menu-button`     | `(props: CourierInboxMenuButtonFactoryProps) => VNodeChild` *(popup menu)* |
| `:render-loading-state`   | `(props: CourierInboxStateLoadingFactoryProps) => VNodeChild`              |
| `:render-empty-state`     | `(props: CourierInboxStateEmptyFactoryProps) => VNodeChild`                |
| `:render-error-state`     | `(props: CourierInboxStateErrorFactoryProps) => VNodeChild`                |
| `:render-pagination-item` | `(props: CourierInboxPaginationItemFactoryProps) => VNodeChild`            |

<Expandable title="Custom list item example">
  ```vue theme={null}
  <script setup lang="ts">
  import { h } from "vue";
  import { CourierInbox, type CourierInboxListItemFactoryProps } from "@trycourier/courier-vue";

  function renderListItem({ message, index }: CourierInboxListItemFactoryProps) {
    return h(
      "pre",
      { style: "padding:24px;border-bottom:1px solid #e0e0e0;margin:0" },
      JSON.stringify({ message, index }, null, 2)
    );
  }
  </script>

  <template>
    <CourierInbox :render-list-item="renderListItem" />
  </template>
  ```
</Expandable>

<Expandable title="Custom header example">
  ```vue theme={null}
  <script setup lang="ts">
  import { h } from "vue";
  import { CourierInbox, type CourierInboxHeaderFactoryProps } from "@trycourier/courier-vue";

  function renderHeader(props: CourierInboxHeaderFactoryProps) {
    const selectedFeed = props.feeds.find((feed) => feed.isSelected);
    const selectedTab = selectedFeed?.tabs.find((tab) => tab.isSelected);
    return h(
      "div",
      { style: "background:red;font-size:24px;padding:24px;width:100%" },
      `Feed: ${selectedFeed?.title ?? "None"} | Tab: ${selectedTab?.title ?? "None"} | Unread: ${selectedTab?.unreadCount ?? 0}`
    );
  }
  </script>

  <template>
    <CourierInbox :render-header="renderHeader" />
  </template>
  ```
</Expandable>

You can also reach the underlying web component for imperative methods (e.g. `removeHeader()`) via a template ref — the component exposes `getElement()`:

```vue theme={null}
<script setup lang="ts">
import { onMounted, ref } from "vue";
import { CourierInbox, type CourierInboxElement } from "@trycourier/courier-vue";

const inbox = ref<{ getElement: () => CourierInboxElement | null } | null>(null);

onMounted(() => {
  inbox.value?.getElement()?.removeHeader();
});
</script>

<template>
  <CourierInbox ref="inbox" />
</template>
```

## Toast Component

### `<CourierToast />`

Toasts are short-lived notifications that notify users and prompt them to take action. The Toast component is connected to the feed of Courier Inbox messages.

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

const courier = useCourier();

onMounted(() => {
  const jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...";
  courier.shared.signIn({ userId: "$YOUR_USER_ID", jwt });
});
</script>

<template>
  <CourierToast />
</template>
```

<Note>
  Some initialization for toasts is asynchronous. If your app displays toasts immediately when
  the component is mounted, use the `@ready` event to wait until the component is fully initialized.
</Note>

### Handle Clicks

| Event                      | Payload                            |
| :------------------------- | :--------------------------------- |
| `@toast-item-click`        | `CourierToastItemClickEvent`       |
| `@toast-item-action-click` | `CourierToastItemActionClickEvent` |

```vue theme={null}
<CourierToast
  :on-toast-item-click="({ message }) => console.log('Toast clicked:', message)"
  :on-toast-item-action-click="({ action }) => window.open(action.href)"
/>
```

### Styles and Theming

```vue theme={null}
<script setup lang="ts">
import { CourierToast, type CourierToastTheme } from "@trycourier/courier-vue";

const theme: CourierToastTheme = {
  item: {
    title: { color: "#6366f1", weight: "bold" },
    backgroundColor: "#edeefc",
    border: "1px solid #cdd1ff",
    borderRadius: "15px",
  },
};
</script>

<template>
  <CourierToast :light-theme="theme" mode="light" />
</template>
```

Toast theme utilities: `defaultToastLightTheme` / `defaultToastDarkTheme` for defaults, and `mergeToastTheme(baseTheme, overrideTheme)` to merge.

### Custom Elements

| Render Prop                  | Description                                                                             |
| :--------------------------- | :-------------------------------------------------------------------------------------- |
| `:render-toast-item-content` | `(props: CourierToastItemFactoryProps) => VNodeChild` — customize the content area only |
| `:render-toast-item`         | `(props: CourierToastItemFactoryProps) => VNodeChild` — fully replace each toast item   |

```vue theme={null}
<script setup lang="ts">
import { h } from "vue";
import { CourierToast, type CourierToastItemFactoryProps } from "@trycourier/courier-vue";

function renderToastItemContent({ message }: CourierToastItemFactoryProps) {
  return h("div", { style: "padding:16px" }, [
    h("strong", { style: "display:block;margin-bottom:4px" }, message.title),
    h("p", { style: "margin:0;font-size:14px;color:#6b7280" }, message.body),
  ]);
}
</script>

<template>
  <CourierToast :render-toast-item-content="renderToastItemContent" />
</template>
```

### CourierToast Props

| Prop Name                | Type                                         | Default                                                                          | Description                                               |
| :----------------------- | :------------------------------------------- | :------------------------------------------------------------------------------- | :-------------------------------------------------------- |
| `style`                  | `StyleValue`                                 | `{ position: "fixed", width: "380px", top: "30px", right: "30px", zIndex: 999 }` | Styles applied to the toast component.                    |
| `lightTheme`             | `CourierToastTheme`                          | `undefined`                                                                      | Theme for light mode.                                     |
| `darkTheme`              | `CourierToastTheme`                          | `undefined`                                                                      | Theme for dark mode.                                      |
| `mode`                   | `"light" \| "dark" \| "system"`              | `"system"`                                                                       | Theme mode.                                               |
| `autoDismiss`            | `boolean`                                    | `false`                                                                          | Enable auto-dismiss with countdown bar.                   |
| `autoDismissTimeoutMs`   | `number`                                     | `5000`                                                                           | Timeout in ms before auto-dismiss.                        |
| `dismissButton`          | `"visible" \| "hidden" \| "hover" \| "auto"` | `"auto"`                                                                         | Dismiss button visibility.                                |
| `onToastItemClick`       | `fn`                                         | `undefined`                                                                      | Callback when a toast is clicked.                         |
| `onToastItemActionClick` | `fn`                                         | `undefined`                                                                      | Callback when a toast action button is clicked.           |
| `renderToastItem`        | `fn`                                         | `undefined`                                                                      | Custom render for entire toast items.                     |
| `renderToastItemContent` | `fn`                                         | `undefined`                                                                      | Custom render for toast item content only.                |
| `onReady`                | `(ready: boolean) => void`                   | `undefined`                                                                      | Callback when the component is ready to receive messages. |

```vue theme={null}
<CourierToast :auto-dismiss="true" :auto-dismiss-timeout-ms="7000" />
```

## Preferences Component

### `<CourierPreferences />`

The Preferences component lets your users manage which topics they're subscribed to and how each topic is delivered (per-channel routing and digest schedules), directly inside your Vue app.

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

const courier = useCourier();

onMounted(() => {
  const jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...";
  courier.shared.signIn({ userId: "$YOUR_USER_ID", jwt });
});
</script>

<template>
  <CourierPreferences />
</template>
```

<Note>
  Authentication requires a JWT that includes the `read:preferences` and `write:preferences` scopes.
</Note>

### CourierPreferences Props

| Prop Name       | Type                            | Default     | Description                                                                 |
| :-------------- | :------------------------------ | :---------- | :-------------------------------------------------------------------------- |
| `style`         | `StyleValue`                    | `undefined` | Styles applied to the preferences component.                                |
| `lightTheme`    | `CourierPreferencesTheme`       | `undefined` | Theme for light mode. Merged with defaults.                                 |
| `darkTheme`     | `CourierPreferencesTheme`       | `undefined` | Theme for dark mode. Merged with defaults.                                  |
| `mode`          | `"light" \| "dark" \| "system"` | `"system"`  | Theme mode.                                                                 |
| `title`         | `string`                        | `undefined` | Override the component's title text.                                        |
| `subtitle`      | `string`                        | `undefined` | Override the component's subtitle text.                                     |
| `brandId`       | `string`                        | `undefined` | Render preferences using a specific brand's styling.                        |
| `channelLabels` | `Record<string, string>`        | `undefined` | Rename how delivery channels appear in the UI (e.g. `{ email: "E-mail" }`). |
| `onError`       | `(error: Error) => void`        | `undefined` | Callback invoked when the component encounters an error.                    |

Preferences theme utilities: `defaultPreferencesLightTheme` / `defaultPreferencesDarkTheme` for defaults, and `mergePreferencesTheme(mode, overrideTheme)` to merge.

## useCourier Composable

The `useCourier()` composable provides programmatic access to Courier functionality for building custom UIs. It returns the same shape as the React hook, but `auth`, `inbox`, and `toast` are Vue [`shallowRef`s](https://vuejs.org/api/reactivity-advanced.html#shallowref) — access them with `.value` in `<script setup>`, and they auto-unwrap in templates.

### When to use the composable vs components

* **Components** (`<CourierInbox />`, `<CourierToast />`): quick integration with default UI
* **Composable** (`useCourier()`): custom UIs, programmatic control, advanced state management
* **Both together**: use the composable for state management while components handle rendering

### Composable return value

```ts theme={null}
const { shared, auth, inbox, toast, preferences } = useCourier();

// In <script setup>, reactive slices need .value:
auth.value.signIn({ userId, jwt });
inbox.value.registerFeeds(defaultFeeds());
await inbox.value.listenForUpdates();
await inbox.value.load();
console.log(inbox.value.totalUnreadCount);

// `shared` and `preferences` are plain (no .value):
const prefs = await preferences.getUserPreferences();
```

<Warning>
  You must call `inbox.value.listenForUpdates()` after authentication to enable real-time message updates. Without this, the inbox only shows messages from the initial load.
</Warning>

**Complete example** — authentication, inbox setup, real-time updates, and displaying messages. Note templates auto-unwrap the refs (`inbox.totalUnreadCount`, `inbox.feeds[...]`):

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

const { auth, inbox } = useCourier();

onMounted(async () => {
  auth.value.signIn({
    userId: "$YOUR_USER_ID",
    jwt: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  });
  inbox.value.registerFeeds(defaultFeeds());
  await inbox.value.listenForUpdates();
  await inbox.value.load();
});
</script>

<template>
  <div>
    <div>Total Unread: {{ inbox.totalUnreadCount ?? 0 }}</div>
    <ul>
      <li
        v-for="message in inbox.feeds['all_messages']?.messages ?? []"
        :key="message.messageId"
        :style="{ backgroundColor: message.read ? 'transparent' : '#fee2e2', padding: '8px', marginBottom: '4px' }"
      >
        {{ message.title }}
      </li>
    </ul>
  </div>
</template>
```

### Inbox methods

| Method                                               | Description                                                                |
| :--------------------------------------------------- | :------------------------------------------------------------------------- |
| `inbox.value.load(props?)`                           | Load messages. Optional `{ canUseCache, datasetIds }`.                     |
| `inbox.value.fetchNextPageOfMessages({ datasetId })` | Fetch next page. Returns `InboxDataSet \| null`.                           |
| `inbox.value.setPaginationLimit(limit)`              | Set messages per page.                                                     |
| `inbox.value.registerFeeds(feeds)`                   | Register feeds and tabs with the datastore.                                |
| `inbox.value.listenForUpdates()`                     | Start WebSocket connection for real-time updates. **Required after auth.** |
| `inbox.value.readMessage(message)`                   | Mark as read.                                                              |
| `inbox.value.unreadMessage(message)`                 | Mark as unread.                                                            |
| `inbox.value.archiveMessage(message)`                | Archive.                                                                   |
| `inbox.value.unarchiveMessage(message)`              | Unarchive.                                                                 |
| `inbox.value.clickMessage(message)`                  | Track click event (analytics).                                             |
| `inbox.value.openMessage(message)`                   | Mark as opened.                                                            |
| `inbox.value.readAllMessages()`                      | Mark all as read.                                                          |

## Advanced

### EU and regional endpoints

Only needed if your workspace uses the [EU datacenter](/docs/platform/workspaces/eu-datacenter). `@trycourier/courier-vue` re-exports `EU_COURIER_API_URLS` and `getCourierApiUrlsForRegion` from `@trycourier/courier-js`.

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

const courier = useCourier();

onMounted(() => {
  courier.shared.signIn({
    userId: "$YOUR_USER_ID",
    jwt: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
    apiUrls: getCourierApiUrlsForRegion("eu"),
  });
});
</script>

<template>
  <CourierInbox />
</template>
```

### Server-side rendering

Courier Inbox and Toast render client-side only. They mount in `onMounted` (which never runs during SSR), so they work with Nuxt and other SSR setups without extra configuration — the components simply render once on the client.

### Troubleshooting

<AccordionGroup>
  <Accordion title="Inbox not updating in real-time">
    Make sure you've called `inbox.value.listenForUpdates()` after authentication. This establishes the WebSocket connection required for real-time updates.

    ```ts theme={null}
    auth.value.signIn({ userId, jwt });
    inbox.value.registerFeeds(defaultFeeds());
    await inbox.value.listenForUpdates(); // Required for real-time
    await inbox.value.load();
    ```
  </Accordion>

  <Accordion title="Messages not loading">
    **Possible causes:**

    1. Not authenticated — ensure `signIn()` has been called
    2. Feeds not registered — call `registerFeeds()` before `load()`
    3. Network errors — check `inbox.value.error` for details
    4. JWT expired — generate a new token
  </Accordion>

  <Accordion title="Authentication errors">
    * **Invalid JWT**: Ensure the JWT is generated correctly on your backend
    * **Expired JWT**: JWTs have an expiration time; generate a new one
    * **Missing scopes**: Ensure your JWT includes `inbox:read:messages` and `inbox:write:events`
    * **Wrong user ID**: Verify the `userId` matches the user the JWT was issued for
  </Accordion>

  <Accordion title="Custom slots not rendering">
    Custom render props must return a Vue node built with `h()` (imported from `vue`), not a raw string of markup. Ensure the render prop is bound with `:render-list-item="..."` (kebab-case) on the component.
  </Accordion>

  <Accordion title="TypeScript errors">
    Import types directly from the package:

    ```ts theme={null}
    import { useCourier, type InboxMessage, type CourierInboxFeed } from "@trycourier/courier-vue";
    ```
  </Accordion>
</AccordionGroup>

### Best Practices

* **JWT Security**: Always generate JWTs server-side. Cache on the client, refresh before expiration (standard: `'1d'`), include only necessary scopes.
* **Performance**: Use `canUseCache: true` (default) for cached data. Set appropriate `setPaginationLimit()` values. The composable's `shallowRef`s update automatically; avoid triggering unnecessary re-renders.
* **Testing**: Mock `useCourier()` in tests to return test data:

```ts theme={null}
import { ref } from "vue";

vi.mock("@trycourier/courier-vue", () => ({
  useCourier: () => ({
    inbox: ref({ feeds: { all_messages: { messages: mockMessages } }, totalUnreadCount: 5 }),
  }),
}));
```

## TypeScript Types

<Expandable title="type InboxMessage">
  ```typescript theme={null}
  type InboxMessage = {
    messageId: string;
    title?: string;
    body?: string;
    read?: string;     // ISO timestamp
    opened?: string;   // ISO timestamp
    archived?: string; // ISO timestamp
    tags?: string[];
    trackingIds?: { clickTrackingId?: string; openTrackingId?: string; };
    actions?: InboxAction[];
    data?: Record<string, unknown>;
  }
  ```
</Expandable>

<Expandable title="type InboxDataSet">
  ```typescript theme={null}
  type InboxDataSet = {
    id: string;
    messages: InboxMessage[];
    unreadCount: number;
    canPaginate: boolean;
    paginationCursor: string | null;
  }
  ```
</Expandable>

<Expandable title="type CourierInboxFeed">
  ```typescript theme={null}
  type CourierInboxFeed = {
    feedId: string;
    title: string;
    iconSVG?: string;
    tabs: CourierInboxTab[];
  }
  ```
</Expandable>

<Expandable title="type CourierInboxTab">
  ```typescript theme={null}
  type CourierInboxTab = {
    datasetId: string;
    title: string;
    filter: CourierInboxDatasetFilter;
  }
  ```
</Expandable>

<Expandable title="type CourierInboxDatasetFilter">
  ```typescript theme={null}
  type CourierInboxDatasetFilter = {
    tags?: string[];
    archived?: boolean;
    status?: 'read' | 'unread';
  }
  ```

  All filter properties are AND'd together. For example, `{ tags: ['important'], status: 'unread' }` shows messages that have the 'important' tag AND are unread.
</Expandable>

<Expandable title="type CourierToastItemFactoryProps">
  ```typescript theme={null}
  type CourierToastItemFactoryProps = {
    message: InboxMessage;
    autoDismiss: boolean;
    autoDismissTimeoutMs: number;
    dismiss: () => void;
  }
  ```
</Expandable>
