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

# Add an inbox to Next.js

> A route handler and a client component: the two files that put a Courier Inbox in Next.js.

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="Inbox" />

Two files get a Courier Inbox working in Next.js: a route handler that mints a token, and a client component that renders the feed.

The route handler exists because your API key signs the token and must never reach the browser. Everything else is standard App Router work.

<Card title="courier-nextjs-quickstart" icon="github" href="https://github.com/trycourier/courier-nextjs-quickstart" horizontal>
  The finished app from this guide, ready to clone and run.
</Card>

## Prerequisites

* <AppLink href="https://app.courier.com/signup">A Courier account</AppLink>
* <AppLink href="https://app.courier.com/settings/api-keys">A Courier API key</AppLink>, from any environment. Every one ships with the Courier Inbox provider already configured.
* A Next.js app on the App Router, and Node 20.9 or newer
* Somewhere to read the signed-in user, such as NextAuth, Clerk, or your own session cookie

## Clone the finished app

Skip the steps below if you would rather read working code. This repo is this guide, already assembled.

```bash theme={null}
git clone https://github.com/trycourier/courier-nextjs-quickstart.git
cd courier-nextjs-quickstart
npm install
cp .env.example .env.local   # paste your API key
npm run dev
```

Open `localhost:3000`, then run `npm run send` in a second terminal. The message arrives in the open page without a refresh.

## Add an inbox to your app

<Steps>
  <Step title="Install the packages">
    The React package renders the inbox. The server package mints tokens.

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

    Put your key in `.env.local`, which Next loads automatically.

    ```bash .env.local theme={null}
    COURIER_API_KEY=YOUR_COURIER_API_KEY
    ```
  </Step>

  <Step title="Add a route handler that mints the token">
    Your API key signs the JWT, so this runs on the server and never in the browser.

    ```tsx app/api/courier/token/route.ts lines theme={null}
    import Courier from "@trycourier/courier";

    export async function GET() {
      const client = new Courier({ apiKey: process.env.COURIER_API_KEY! });

      // Read the user from your own session. Never from the request.
      const session = await auth();
      if (!session) return new Response("Unauthorized", { status: 401 });

      const userId = session.user.id;

      const { token } = await client.auth.issueToken({
        scope: `user_id:${userId} inbox:read:messages inbox:write:events`,
        expires_in: "1 day",
      });

      return Response.json({ userId, token });
    }
    ```

    Take the user id from your session, never from the request body or a query parameter. A caller who can name any user can read that user's inbox.

    The route calls <Endpoint method="POST" path="/auth/issue-token" name="Create a JWT" href="/docs/api-reference/authentication/create-a-jwt" />, and the two `inbox:` scopes are the minimum for a working feed. <Doc href="/docs/in-app/authenticate-users#scopes">Scopes</Doc> lists what to add for preferences and push.
  </Step>

  <Step title="Render the inbox in a client component">
    Fetch the token, call `signIn`, then mount `CourierInbox`.

    ```tsx components/courier-inbox.tsx lines theme={null}
    "use client";

    import { useEffect, useRef } from "react";
    import { CourierInbox, useCourier } from "@trycourier/courier-react";

    export function Inbox() {
      const courier = useCourier();
      const signedIn = useRef(false);

      useEffect(() => {
        if (signedIn.current) return;
        signedIn.current = true;

        fetch("/api/courier/token")
          .then((response) => response.json())
          .then(({ userId, token }) => courier.shared.signIn({ userId, jwt: token }));
      }, [courier]);

      return <CourierInbox />;
    }
    ```

    Two details are specific to Next.js and React:

    * **`"use client"` is required.** The component uses hooks, and the inbox renders as a custom element that only exists in the browser.
    * **Effects run twice in development.** A second `signIn` signs the first user out again, so guard it with a ref.
  </Step>

  <Step title="Give the inbox a height">
    `CourierInbox` fills its container's width and takes its height from the parent, so size the parent.

    ```tsx app/page.tsx theme={null}
    import { Inbox } from "@/components/courier-inbox";

    export default function Home() {
      return (
        <main style={{ height: "32rem" }}>
          <Inbox />
        </main>
      );
    }
    ```

    <Note>
      On React 17, install `@trycourier/courier-react-17` instead. The API is identical.
    </Note>
  </Step>

  <Step title="Send a message to the inbox">
    `inbox` is a channel like `email` or `sms`. Including it in `routing.channels` is what puts the message in the feed.

    ```tsx scripts/send.ts theme={null}
    import Courier from "@trycourier/courier";

    const client = new Courier({ apiKey: process.env.COURIER_API_KEY! });

    await client.send.message({
      message: {
        to: { user_id: "sarah-bennett" },
        routing: { method: "single", channels: ["inbox"] },
        content: { title: "Your report is ready", body: "The export has finished." },
      },
    });
    ```

    `content` here is the shorthand form, which takes a `title` and a `body`. For multi-element layouts and per-channel variants, use a <Doc href="/docs/design/templates/overview">template</Doc> instead and reference it by id.
  </Step>
</Steps>

## You do not need `ssr: false`

Wrapping the inbox in `next/dynamic` with `ssr: false` is a common reflex with component libraries built on custom elements. Courier's SDK does not need it.

The element base class resolves to a stub under Node, and element registration is guarded on `typeof window`. Importing the package on the server is safe, so the page still prerenders and only the inbox hydrates in the browser.

`"use client"` is the whole requirement. Adding `ssr: false` costs you the prerender and buys nothing.

## Keep the session alive

The SDKs do not refresh tokens. Before the current one expires, mint a new JWT and call `signIn` again with it.

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

<Warning>
  `expires_in` is optional, and omitting it mints a token that **never expires**. Always set it. Rotating the API key that signed it is the only way to revoke one.
</Warning>

## Verify

<Steps>
  <Step title="Load the page">
    The inbox renders, signed in and empty.
  </Step>

  <Step title="Send a message">
    Run the send against the same `user_id` your token was scoped to.
  </Step>

  <Step title="Watch it arrive">
    The message appears in the open page in real time, and the unread count moves.
  </Step>
</Steps>

If the inbox renders but stays empty, suspect the token before anything else. An expired or mis-scoped JWT signs in silently and returns no messages. <Doc href="/docs/in-app/authenticate-users#troubleshooting">Troubleshooting</Doc> walks the four causes in order.

## FAQ

<AccordionGroup>
  <Accordion title="Can I mint the token in a Server Component instead?">
    Minting it in a Server Component works, and passing it to the client component as a prop saves a round trip. A route handler is shown here because it is also what you call again when the token expires.
  </Accordion>

  <Accordion title="Does this work with the Pages Router?">
    Mint the token in `pages/api/courier/token.ts` and render the inbox in any component. `"use client"` is an App Router directive, and the Pages Router ignores it.
  </Accordion>

  <Accordion title="Is the JWT safe to expose to the browser?">
    The JWT is built for the browser. It is scoped to one user and it expires. Your API key, which signs it, stays on the server.
  </Accordion>

  <Accordion title="Why is the inbox empty when the send reported success?">
    The feed is filtered by tenant. Signing in with a `tenantId` hides messages sent without one. See <Doc href="/docs/in-app/send-to-the-inbox#scope-the-inbox-to-a-tenant">scope the inbox to a tenant</Doc>.
  </Accordion>
</AccordionGroup>
