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

> Everything an AI coding agent needs to send email and in-app inbox notifications with Courier from a brand-new workspace.

<Note>
  This page is written for AI coding agents. It gives you everything you need to send email and in-app inbox notifications with Courier without any prior context. Read it top to bottom before writing any code.
</Note>

## Quick Reference: Copy for Cursor / Claude

Paste one of these blocks into your coding agent when you need a self-contained starting point.

<CodeGroup>
  ```javascript Node.js theme={null}
  import Courier from "@trycourier/courier";

  /**
   * Setup:
   *   npm install @trycourier/courier
   *   export COURIER_API_KEY="your-api-key"
   *
   * Guardrails:
   * - POST /profiles/{user_id} merges profile fields; PUT replaces and can delete fields.
   * - For OTP/billing flows, use routing.method = "single" and include Idempotency-Key.
   * - Pass Idempotency-Key through request headers in Node.
   */
  const client = new Courier();

  await client.profiles.create("user-123", {
    profile: { email: "alice@example.com", name: "Alice" },
  });

  const sendResult = await client.send.message(
    {
      message: {
        to: { user_id: "user-123" },
        content: {
          title: "Your order shipped",
          body: "Order #{{orderId}} is on its way.",
        },
        data: { orderId: "ORD-456" },
        routing: { method: "single", channels: ["email", "sms"] },
      },
    },
    { headers: { "Idempotency-Key": "order-ORD-456-user-123" } }
  );

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

  console.log({ requestId: sendResult.requestId, inboxJwt: token });
  ```

  ```python Python theme={null}
  from courier import Courier

  """
  Setup:
    pip install trycourier
    export COURIER_API_KEY="your-api-key"

  Guardrails:
  - POST /profiles/{user_id} merges profile fields; PUT replaces and can delete fields.
  - For OTP/billing flows, use routing.method = "single" and include Idempotency-Key.
  """
  client = Courier()

  client.profiles.create(
      "user-123",
      profile={"email": "alice@example.com", "name": "Alice"},
  )

  send_result = client.send.message(
      message={
          "to": {"user_id": "user-123"},
          "content": {
              "title": "Your order shipped",
              "body": "Order #{{orderId}} is on its way.",
          },
          "data": {"orderId": "ORD-456"},
          "routing": {"method": "single", "channels": ["email", "sms"]},
      },
      extra_headers={"Idempotency-Key": "order-ORD-456-user-123"},
  )

  auth_response = client.auth.issue_token(
      scope="user_id:user-123 inbox:read:messages inbox:write:events",
      expires_in="1 day",
  )

  print({"request_id": send_result.request_id, "inbox_jwt": auth_response.token})
  ```
</CodeGroup>

## What you need before writing any code

1. **A Courier account** — [sign up here](https://app.courier.com/signup) if the user hasn't already
2. **An API key** — find it at [Settings > API Keys](https://app.courier.com/settings/api-keys)
3. **A provider connected** (optional for first test) — Courier's built-in email provider and Courier Inbox work with no setup in Test mode. Additional providers (SendGrid, Twilio, etc.) can be connected later at [Integrations](https://app.courier.com/integrations)

Set the API key as an environment variable. All examples below assume this:

```bash theme={null}
export COURIER_API_KEY="your-api-key-here"
```

The Node.js SDK reads `COURIER_API_KEY` from `process.env` automatically. The SDK uses ES module `import` syntax, so your `package.json` needs `"type": "module"` (or use `.mjs` file extensions). If you load env vars from a `.env` file, install `dotenv` and call `import "dotenv/config"` before importing the SDK.

***

## Core concepts

### Environments

Every workspace has two environments: **Test** and **Production**. They use different API keys. The key from Settings > API Keys defaults to Test. Sending to your own email from Test mode is fine for validation; do not send to end users from Test.

### Recipients

The `to` field accepts five alternative shapes. Use whichever the user's system already has:

* `{ "user_id": "user-123" }` — looks up a stored profile for contact details (preferred)
* `{ "email": "alice@example.com" }` — ad-hoc email, no profile needed
* `{ "phone_number": "+14155550123" }` — ad-hoc SMS (E.164 format required)
* `{ "list_id": "weekly-digest" }` — sends to all list subscribers
* `{ "audience_id": "churned-users" }` — sends to a dynamic audience

### Routing

The `routing` object controls which channels Courier tries:

```json theme={null}
{ "method": "all", "channels": ["email", "inbox"] }
```

* `"all"` — delivers to every channel in the list simultaneously
* `"single"` — tries channels left to right, stops at first success (fallback chain)

If you omit `routing`, Courier uses the notification template's configured routing.

### Inline content vs templates

Two ways to provide message content:

**Inline** — content is in the API call itself. Good for dynamic or one-off messages:

```json theme={null}
"content": { "title": "Your order shipped", "body": "Order #{{orderId}} is on its way." }
```

**Template** — content lives in Courier's [Design Studio](/platform/content/design-studio/design-studio-overview), referenced by ID. Good for messages that need consistent branding or visual design:

```json theme={null}
"template": "order-shipped"
```

Both work the same way at the API level. Templates give non-engineers control over copy and design without code changes. You can also [create and manage templates programmatically](/platform/content/design-studio/manage-templates-api) via `POST /notifications` and `PUT /notifications/{id}`.

### Profiles

A profile is a JSON object stored per `user_id` that holds contact info and attributes:

```json theme={null}
{
  "email": "alice@example.com",
  "phone_number": "+14155550123",
  "name": "Alice"
}
```

Courier merges send-time data with stored profiles. If you send to `user_id: "user-123"` and that user has a stored email, Courier uses it automatically.

<Warning>
  Use `POST /profiles/{user_id}` (merge) to create or update profiles. `PUT /profiles/{user_id}` replaces the entire profile; any field not included in the PUT body is deleted.
</Warning>

### Idempotency

For transactional notifications (OTPs, order confirmations, billing alerts), always pass an `Idempotency-Key` header. If the same key is sent twice, Courier returns the stored response without re-sending. This includes error responses; replaying a key that failed does not retry with a corrected payload.

<CodeGroup>
  ```bash cURL theme={null}
  -H "Idempotency-Key: order-shipped-order-456-user-123"
  ```

  ```javascript Node.js theme={null}
  await client.send.message(
    { message: { /* ... */ } },
    { headers: { "Idempotency-Key": "order-shipped-order-456-user-123" } }
  );
  ```

  ```python Python theme={null}
  client.send.message(
      message={ ... },
      extra_headers={"Idempotency-Key": "order-shipped-order-456-user-123"},
  )
  ```
</CodeGroup>

<Warning>
  In the Node SDK, the `idempotencyKey` request option exists but silently does nothing; the SDK does not map it to a header. Always pass the key via `headers` as shown above.
</Warning>

***

## Step 1: Send your first message

This sends an email via the Courier Send API. In Test mode, Courier's built-in email provider is available with no integration setup, but delivery depends on a valid recipient address and workspace email configuration (from address, brand).

<Warning>
  Use your own email address, not `test@example.com`. Reserved domain addresses are blocked by email providers and will result in an `UNDELIVERABLE` status.
</Warning>

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST https://api.courier.com/send \
    -H "Authorization: Bearer $COURIER_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{
      "message": {
        "to": { "email": "you@yourdomain.com" },
        "content": {
          "title": "Hello from Courier",
          "body": "Your first notification."
        },
        "routing": {
          "method": "single",
          "channels": ["email"]
        }
      }
    }'
  ```

  ```bash CLI theme={null}
  # Install: npm install -g @trycourier/cli
  courier send message \
    --message '{"to":{"email":"you@yourdomain.com"},"content":{"title":"Hello from Courier","body":"Your first notification."},"routing":{"method":"single","channels":["email"]}}'
  ```

  ```javascript Node.js theme={null}
  // npm install @trycourier/courier
  // Both default and named imports work:
  //   import Courier from "@trycourier/courier"
  //   import { Courier } from "@trycourier/courier"
  import Courier from "@trycourier/courier";

  const client = new Courier();

  const response = await client.send.message({
    message: {
      to: { email: "you@yourdomain.com" },
      content: {
        title: "Hello from Courier",
        body: "Your first notification.",
      },
      routing: { method: "single", channels: ["email"] },
    },
  });

  console.log("Sent:", response.requestId);
  ```

  ```python Python theme={null}
  # pip install trycourier
  from courier import Courier

  client = Courier()

  response = client.send.message(
      message={
          "to": {"email": "you@yourdomain.com"},
          "content": {
              "title": "Hello from Courier",
              "body": "Your first notification.",
          },
          "routing": {"method": "single", "channels": ["email"]},
      }
  )

  print("Sent:", response.request_id)
  ```
</CodeGroup>

A `200` response with a `requestId` means Courier accepted the job; it does not mean the message was delivered. Use `GET /messages/{requestId}` or [Message Logs](https://app.courier.com/logs) to confirm per-provider delivery status.

```json theme={null}
{ "requestId": "1-abc123-def456" }
```

***

## Step 2: Store a user profile

Store contact information so you can send to `user_id` without repeating details in every call:

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST https://api.courier.com/profiles/user-123 \
    -H "Authorization: Bearer $COURIER_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{ "profile": { "email": "alice@example.com", "phone_number": "+14155550123", "name": "Alice" } }'
  ```

  ```javascript Node.js theme={null}
  // using client from Step 1
  await client.profiles.create("user-123", {
    profile: {
      email: "alice@example.com",
      phone_number: "+14155550123",
      name: "Alice",
    },
  });
  ```

  ```python Python theme={null}
  # using client from Step 1
  client.profiles.create(
      "user-123",
      profile={
          "email": "alice@example.com",
          "phone_number": "+14155550123",
          "name": "Alice",
      },
  )
  ```
</CodeGroup>

Then send to that user by ID: `"to": { "user_id": "user-123" }`. Courier looks up the stored profile and routes automatically.

***

## Step 3: Send with dynamic data and idempotency

For real transactional sends, pass dynamic data via the `data` object and include an idempotency key. Use `{{variable}}` placeholders in your content to interpolate values:

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST https://api.courier.com/send \
    -H "Authorization: Bearer $COURIER_API_KEY" \
    -H "Content-Type: application/json" \
    -H "Idempotency-Key: order-shipped-ORD-456-user-123" \
    -d '{
      "message": {
        "to": { "user_id": "user-123" },
        "content": {
          "title": "Your order shipped",
          "body": "Order #{{orderId}} is on its way. Track it here: {{trackingUrl}}"
        },
        "data": { "orderId": "ORD-456", "trackingUrl": "https://track.example.com/ORD-456" },
        "routing": { "method": "all", "channels": ["email", "inbox"] }
      }
    }'
  ```

  ```javascript Node.js theme={null}
  // using client from Step 1
  const response = await client.send.message(
    {
      message: {
        to: { user_id: "user-123" },
        content: {
          title: "Your order shipped",
          body: "Order #{{orderId}} is on its way. Track it here: {{trackingUrl}}",
        },
        data: { orderId: "ORD-456", trackingUrl: "https://track.example.com/ORD-456" },
        routing: { method: "all", channels: ["email", "inbox"] },
      },
    },
    { headers: { "Idempotency-Key": "order-shipped-ORD-456-user-123" } }
  );
  ```

  ```python Python theme={null}
  # using client from Step 1
  response = client.send.message(
      message={
          "to": {"user_id": "user-123"},
          "content": {
              "title": "Your order shipped",
              "body": "Order #{{orderId}} is on its way. Track it here: {{trackingUrl}}",
          },
          "data": {"orderId": "ORD-456", "trackingUrl": "https://track.example.com/ORD-456"},
          "routing": {"method": "all", "channels": ["email", "inbox"]},
      },
      extra_headers={"Idempotency-Key": "order-shipped-ORD-456-user-123"},
  )
  ```
</CodeGroup>

**Using a template instead of inline content**: If you have a template created in [Design Studio](https://app.courier.com/designer) or via the [Templates API](/platform/content/design-studio/manage-templates-api), replace the `content` field with `"template": "your-template-id"`. The `data` object is passed to the template for variable interpolation. If the template ID doesn't exist, the API returns `{"status":"UNMAPPED"}` with no error message.

***

## Step 4: Send to Inbox (in-app notifications)

Courier Inbox is an in-app notification feed. Messages sent to the `inbox` channel appear in a real-time UI component embedded in the user's app. No external provider is needed; the Courier Inbox provider is built in.

### Send a message to Inbox

Target the `inbox` channel in routing. Inbox requires `user_id` (not ad-hoc email), because the message is tied to a user's feed:

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST https://api.courier.com/send \
    -H "Authorization: Bearer $COURIER_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{
      "message": {
        "to": { "user_id": "user-123" },
        "content": {
          "title": "New activity",
          "body": "Your report is ready to download."
        },
        "routing": {
          "method": "all",
          "channels": ["email", "inbox"]
        }
      }
    }'
  ```

  ```javascript Node.js theme={null}
  // using client from Step 1
  const response = await client.send.message({
    message: {
      to: { user_id: "user-123" },
      content: {
        title: "New activity",
        body: "Your report is ready to download.",
      },
      routing: { method: "all", channels: ["email", "inbox"] },
    },
  });
  ```

  ```python Python theme={null}
  # using client from Step 1
  response = client.send.message(
      message={
          "to": {"user_id": "user-123"},
          "content": {
              "title": "New activity",
              "body": "Your report is ready to download.",
          },
          "routing": {"method": "all", "channels": ["email", "inbox"]},
      },
  )
  ```
</CodeGroup>

This delivers to both email and inbox simultaneously. The user sees the message in their inbox feed and receives an email.

### Authenticate users for Inbox

Inbox SDKs use JWTs to authenticate. Your backend issues a token per user via the Courier auth endpoint:

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST https://api.courier.com/auth/issue-token \
    -H "Authorization: Bearer $COURIER_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{
      "scope": "user_id:user-123 inbox:read:messages inbox:write:events",
      "expires_in": "1 day"
    }'
  ```

  ```javascript Node.js theme={null}
  // using client from Step 1
  const { token } = await client.auth.issueToken({
    scope: "user_id:user-123 inbox:read:messages inbox:write:events",
    expires_in: "1 day",
  });
  ```

  ```python Python theme={null}
  # using client from Step 1
  response = client.auth.issue_token(
      scope="user_id:user-123 inbox:read:messages inbox:write:events",
      expires_in="1 day",
  )
  token = response.token
  ```
</CodeGroup>

The response contains a `token` field. Pass this JWT to the client-side SDK.

### Embed the Inbox UI

Install a Courier Inbox SDK and render the component. The React example:

```jsx theme={null}
import { useCourier, CourierInbox } from "@trycourier/courier-react";

function NotificationCenter() {
  const courier = useCourier();

  useEffect(() => {
    courier.shared.signIn({ userId: "user-123", jwt: jwtFromBackend });
  }, []);

  return <CourierInbox />;
}
```

Inbox SDKs are available for [React](/sdk-libraries/courier-react-web), [Web Components](/sdk-libraries/courier-ui-inbox-web), [React Native](/sdk-libraries/react-native), [iOS](/sdk-libraries/ios), [Android](/sdk-libraries/android), and [Flutter](/sdk-libraries/flutter). See [Inbox overview](/platform/inbox/inbox-overview) for full setup.

***

## Step 5: Debug a delivery

Check why a message was or wasn't delivered:

```bash theme={null}
# Get delivery status
courier messages retrieve --message-id "1-abc123"

# Get full event timeline (enqueued → routed → rendered → sent → delivered)
courier messages history --message-id "1-abc123"
```

Common delivery failure causes:

* **No provider configured** for the channel; add one at [Integrations](https://app.courier.com/integrations)
* **Missing contact info**; the user profile has no email/phone for the channel being attempted
* **Wrong environment**; Test API key used with production user data, or vice versa
* **Provider credentials invalid**; check the integration config in the dashboard

***

## What NOT to do

* **Don't use `PUT /profiles/{user_id}`** unless you intend to replace the entire profile. Any field not included in the PUT body is deleted. Use `POST /profiles/{user_id}` (merge) for updates.
* **Don't omit idempotency keys** on transactional sends. Retries without idempotency keys cause duplicate notifications.
* **Don't use `method: "all"`** for transactional notifications (OTP, password reset, billing). Use `"single"` with fallback channels.
* **Don't send to `user_id` without a stored profile** if the channel requires contact info. Courier won't know where to deliver.
* **Don't commit API keys** to source code. Use environment variables or a secrets manager.

***

## Key API endpoints and SDKs

**Install:** `npm install @trycourier/courier` (Node), `pip install trycourier` (Python), `go get github.com/trycourier/courier-go/v4` (Go), `npm install -g @trycourier/cli` (CLI)

| Operation                                      | Method | Path                                       |
| ---------------------------------------------- | ------ | ------------------------------------------ |
| Send a message                                 | `POST` | `/send`                                    |
| Get message status                             | `GET`  | `/messages/{message_id}`                   |
| Get message history                            | `GET`  | `/messages/{message_id}/history`           |
| Create/merge a profile                         | `POST` | `/profiles/{user_id}`                      |
| Replace a profile                              | `PUT`  | `/profiles/{user_id}`                      |
| Get a profile                                  | `GET`  | `/profiles/{user_id}`                      |
| Issue a JWT (for Inbox auth)                   | `POST` | `/auth/issue-token`                        |
| Get inbox messages                             | `GET`  | `/inbox/messages`                          |
| Subscribe user to list                         | `PUT`  | `/lists/{list_id}/subscriptions/{user_id}` |
| List notification templates                    | `GET`  | `/notifications`                           |
| Create a template (Design Studio / Elemental)  | `POST` | `/notifications`                           |
| Replace a template (Design Studio / Elemental) | `PUT`  | `/notifications/{id}`                      |
| Publish a template                             | `POST` | `/notifications/{id}/publish`              |
| Cancel a pending message                       | `POST` | `/messages/{message_id}/cancel`            |

Full reference: [API Reference](/reference/get-started)

***

For MCP server, CLI, and Skills setup, see [Build with AI](/tools/ai-onboarding).

***

## What's Next

<CardGroup cols={2}>
  <Card title="Inbox Overview" icon="inbox" href="/platform/inbox/inbox-overview">
    Full setup guide for in-app notifications across web and mobile.
  </Card>

  <Card title="MCP Server" icon="plug" href="/tools/mcp">
    Structured tool access for AI agents in Cursor, Claude Code, and Windsurf.
  </Card>

  <Card title="Courier CLI" icon="terminal" href="/tools/cli">
    All API endpoints from the command line.
  </Card>

  <Card title="API Reference" icon="code" href="/reference/get-started">
    Full REST API documentation.
  </Card>
</CardGroup>
