> ## 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 multiple environments and each environment has its own API keys; start with Test.
> 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.

# Send notifications with Astro

> Send from an Astro endpoint and add a real-time in-app inbox as a React island.

The [Node.js quickstart](/docs/quickstarts/node) covers the calls themselves: create a user, send a notification, route it, start a journey. This page is the Astro wiring around them. How to read the API key as a runtime secret, how an endpoint sends, and how to render an in-app inbox as a React island without the key ever reaching the browser. It follows the [Astro quickstart repo](https://github.com/trycourier/courier-astro-quickstart), which you can clone and run in about five minutes.

**What you need**

* The [Node.js quickstart](/docs/quickstarts/node) done, or at least a Courier API key and a user in Courier.
* An Astro app on Node 20.9 or newer with a server adapter, plus `@trycourier/courier`. Add `@astrojs/react` and `@trycourier/courier-react` for the inbox.

## 1. Declare the key as a server secret

Read the key through `astro:env`, not `import.meta.env`. `access: "secret"` reads the environment on every request; `import.meta.env.COURIER_API_KEY` compiles to the build-time value as a string literal and bakes the key into the bundle you deploy. Do not prefix it with `PUBLIC_`, which Astro inlines into the browser build.

```javascript theme={null}
// astro.config.mjs
env: {
  schema: {
    COURIER_API_KEY: envField.string({ context: "server", access: "secret", optional: true }),
  },
},
```

## 2. Send from an endpoint

Endpoints run on the server, so this is where a Courier call belongs. Set `prerender = false` so the route runs per request instead of being built once. The template lives in Design Studio, so copy and channel routing change without a deploy.

```typescript theme={null}
// src/pages/api/notify.ts
import type { APIRoute } from "astro";
import { COURIER_API_KEY } from "astro:env/server";
import Courier from "@trycourier/courier";

export const prerender = false;

export const POST: APIRoute = async ({ request }) => {
  const { userId, plan } = await request.json();
  const client = new Courier({ apiKey: COURIER_API_KEY });

  const { requestId } = await client.send.message(
    { message: { template: "welcome", to: { user_id: userId }, data: { plan } } },
    { idempotencyKey: `welcome-${userId}` }
  );

  return Response.json({ requestId });
};
```

## 3. Add the inbox: token endpoint plus island

The inbox runs in the browser, so it gets a JWT scoped to one user and expiring, signed by your key on the server. The repo mints it in `src/pages/api/courier/token.ts` and reads the user id from the session, never from the request, because a caller who can name any user can read that user's inbox.

```typescript theme={null}
// src/pages/api/courier/token.ts
export const prerender = false;

export const GET: APIRoute = async () => {
  const userId = "sarah-bennett"; // from your session, not the request
  const client = new Courier({ apiKey: COURIER_API_KEY });

  const { token } = await client.auth.issueToken({
    scope: `user_id:${userId} inbox:read:messages inbox:write:events`,
    // Always set this. Omitting it mints a token that never expires.
    expires_in: "1 day",
  });

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

The React island fetches that token and signs in inside an effect, guarded because React runs effects twice in development.

```tsx theme={null}
// src/components/CourierInbox.tsx
import { useEffect, useRef } from "react";
import { CourierInbox, useCourier } from "@trycourier/courier-react";

export default 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((body) => courier.shared.signIn({ userId: body.userId, jwt: body.token }));
  }, [courier]);

  return <CourierInbox />;
}
```

Mount it with `client:only="react"`. The inbox renders as a custom element, so a server pass would render nothing and then throw it away.

```astro theme={null}
---
import Inbox from "../components/CourierInbox";
---

<Inbox client:only="react" />
```

## Next steps

* [Astro quickstart repo](https://github.com/trycourier/courier-astro-quickstart): clone it, add a key, run `npm run dev` and `npm run send`.
* [Node.js quickstart](/docs/quickstarts/node): users, routing, journeys, delivery status.
* [In-app inbox](/docs/platform/inbox/inbox-overview): scopes, tenants, and the rest of the inbox.
* [Node.js SDK reference](/docs/sdk-libraries/node).
* [React SDK reference](/docs/sdk-libraries/courier-react-web).
