Skip to main content
Courier Vue Inbox 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
See these components in action with the interactive Inbox demo — no setup required.

Installation

Available on GitHub and npm.
npm install @trycourier/courier-vue
vue (>= 3.3) is a peer dependency.
Not using Vue? Check out the @trycourier/courier-ui-inbox and @trycourier/courier-ui-toast packages instead, which provide Web Components for any JavaScript project.

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

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).
2

Your backend calls Courier

In your backend endpoint, use your Courier API Key to call the Courier Issue Token Endpoint and generate a JWT for the user.
3

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.
For a step-by-step walkthrough of authentication and token generation, see our JWT authentication tutorial.

Development testing with cURL

To quickly test JWT generation for development only, you can call the Issue Token Endpoint directly.
Do not call the Issue Token API from client-side code. Always keep your Courier API keys secure.
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.
<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>
Follow the inbox implementation tutorial for detailed step-by-step guidance including backend JWT generation.

Inbox Component

<CourierInbox />

<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>
If you’re using tenants, scope requests to a particular tenant by passing its ID to signIn:
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.

<CourierInboxPopupMenu />

<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.
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.
Filter options for each tab:
Filter PropertyTypeDescription
tagsstring[]Messages that have any of the specified tags
archivedbooleanWhether to include archived messages (defaults to false if unset)
status'read' | 'unread'Filter by read/unread status
<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.
EventPayload
@message-clickCourierInboxListItemFactoryProps
@message-action-clickCourierInboxListItemActionFactoryProps
@message-long-pressCourierInboxListItemFactoryProps
message-long-press is only applicable on devices that support touch events.
<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.
<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 (the theme object is identical across SDKs).
Vertical AlignmentOptions
Top"top-right", "top-center", "top-left"
Center"center-right", "center-center", "center-left"
Bottom"bottom-right", "bottom-center", "bottom-left"
<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:
<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 PropType 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
You can also reach the underlying web component for imperative methods (e.g. removeHeader()) via a template ref — the component exposes getElement():
<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.
<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>
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.

Handle Clicks

EventPayload
@toast-item-clickCourierToastItemClickEvent
@toast-item-action-clickCourierToastItemActionClickEvent
<CourierToast
  :on-toast-item-click="({ message }) => console.log('Toast clicked:', message)"
  :on-toast-item-action-click="({ action }) => window.open(action.href)"
/>

Styles and Theming

<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 PropDescription
:render-toast-item-content(props: CourierToastItemFactoryProps) => VNodeChild — customize the content area only
:render-toast-item(props: CourierToastItemFactoryProps) => VNodeChild — fully replace each toast item
<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 NameTypeDefaultDescription
styleStyleValue{ position: "fixed", width: "380px", top: "30px", right: "30px", zIndex: 999 }Styles applied to the toast component.
lightThemeCourierToastThemeundefinedTheme for light mode.
darkThemeCourierToastThemeundefinedTheme for dark mode.
mode"light" | "dark" | "system""system"Theme mode.
autoDismissbooleanfalseEnable auto-dismiss with countdown bar.
autoDismissTimeoutMsnumber5000Timeout in ms before auto-dismiss.
dismissButton"visible" | "hidden" | "hover" | "auto""auto"Dismiss button visibility.
onToastItemClickfnundefinedCallback when a toast is clicked.
onToastItemActionClickfnundefinedCallback when a toast action button is clicked.
renderToastItemfnundefinedCustom render for entire toast items.
renderToastItemContentfnundefinedCustom render for toast item content only.
onReady(ready: boolean) => voidundefinedCallback when the component is ready to receive messages.
<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.
<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>
Authentication requires a JWT that includes the read:preferences and write:preferences scopes.

CourierPreferences Props

Prop NameTypeDefaultDescription
styleStyleValueundefinedStyles applied to the preferences component.
lightThemeCourierPreferencesThemeundefinedTheme for light mode. Merged with defaults.
darkThemeCourierPreferencesThemeundefinedTheme for dark mode. Merged with defaults.
mode"light" | "dark" | "system""system"Theme mode.
titlestringundefinedOverride the component’s title text.
subtitlestringundefinedOverride the component’s subtitle text.
brandIdstringundefinedRender preferences using a specific brand’s styling.
channelLabelsRecord<string, string>undefinedRename how delivery channels appear in the UI (e.g. { email: "E-mail" }).
onError(error: Error) => voidundefinedCallback 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 shallowRefs — 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

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();
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.
Complete example — authentication, inbox setup, real-time updates, and displaying messages. Note templates auto-unwrap the refs (inbox.totalUnreadCount, inbox.feeds[...]):
<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

MethodDescription
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. @trycourier/courier-vue re-exports EU_COURIER_API_URLS and getCourierApiUrlsForRegion from @trycourier/courier-js.
<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

Make sure you’ve called inbox.value.listenForUpdates() after authentication. This establishes the WebSocket connection required for real-time updates.
auth.value.signIn({ userId, jwt });
inbox.value.registerFeeds(defaultFeeds());
await inbox.value.listenForUpdates(); // Required for real-time
await inbox.value.load();
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
  • 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
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.
Import types directly from the package:
import { useCourier, type InboxMessage, type CourierInboxFeed } from "@trycourier/courier-vue";

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 shallowRefs update automatically; avoid triggering unnecessary re-renders.
  • Testing: Mock useCourier() in tests to return test data:
import { ref } from "vue";

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

TypeScript Types