> ## 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` from the Node SDK (`@trycourier/courier` v7 and later, where the client is the default import). 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.
> To translate a template, write one locale at a time with `PUT /notifications/{id}/locales/{localeId}`, which merges into the existing translations. `PUT /notifications/{id}/content` and `PUT /notifications/{id}/elements/{elementId}` overwrite translations along with the content, so send each element's `id` and `locales` back, and first remove every `checksum` and any `locales` key that starts with an underscore.
> 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.

# Send from the Claude Agent SDK

> Let an AI agent built on the Claude Agent SDK notify customers through Courier, with a custom send tool or the Courier MCP server.

Your AI agent decides who to tell and when, and Courier handles channels, routing, failover, preferences, and delivery logs.

Give the agent Courier in one of two ways:

* **A Courier tool** gives you tight control. You write one function that sends one published template, and the agent fills in only the fields your schema allows.
* **The Courier MCP server** gives the agent the full API: 170+ tools for sending, users, templates, and logs. You choose which of them run without a prompt.

> [!TIP]
>
> Build with a **Test** environment key. Test's built-in email provider only delivers to the address you signed up with, so until you connect your own provider, an agent can't reach a real customer by mistake.

## Prerequisites

* [A Courier Test API key](https://app.courier.com/~/test/platform/api-keys)
* [A published template
  DOC](https://www.courier.com/docs/design/templates/overview.md)
* [A user profile with contact info, or the inbox channel
  GUIDE](https://www.courier.com/docs/guides/import-your-users.md)
* [An Anthropic API key](https://platform.claude.com/)
* Node.js 20 or later, or Python 3.10 or later

## Set up the project

In a new folder, install the Agent SDK and the Courier SDK:

```bash npm theme={null}
npm init -y && npm pkg set type=module
npm install @anthropic-ai/claude-agent-sdk @trycourier/courier zod
npm install --save-dev tsx
```

```bash pip theme={null}
python3 -m venv .venv && source .venv/bin/activate
pip install claude-agent-sdk trycourier
```

Set both keys in the shell that runs the agent. Each SDK reads its key from the environment, and neither loads a `.env` file on its own:

```bash theme={null}
export ANTHROPIC_API_KEY="YOUR_ANTHROPIC_API_KEY"
export COURIER_API_KEY="YOUR_COURIER_API_KEY"
```

## Give the agent a Courier tool

**1. Write the agent**

The tool sends one template and takes three inputs. The schema rejects any status other than `in_progress` or `resolved`, so the agent can't invent one. Save the file as `agent.ts` or `agent.py`.

```typescript TypeScript lines theme={null}
import { createSdkMcpServer, query, tool } from "@anthropic-ai/claude-agent-sdk";
import { randomUUID } from "node:crypto";
import Courier from "@trycourier/courier";
import { z } from "zod";

// Reads COURIER_API_KEY from the environment.
const courier = new Courier();

const sendTicketUpdate = tool(
  "send_ticket_update",
  "Tell a customer that their support ticket changed status.",
  {
    user_id: z.string().describe("The customer's Courier user ID"),
    ticket_id: z.string().describe("The support ticket ID"),
    status: z.enum(["in_progress", "resolved"]),
  },
  async ({ user_id, ticket_id, status }) => {
    const { requestId } = await courier.send.message({
      message: {
        to: { user_id },
        template: "nt_01kx4h2jdafq8bk9aftxak4b40",
        data: { ticket_id, status },
      },
      "Idempotency-Key": randomUUID(),
    });
    return { content: [{ type: "text", text: `Sent. requestId: ${requestId}` }] };
  },
);

const notifications = createSdkMcpServer({
  name: "notifications",
  version: "1.0.0",
  tools: [sendTicketUpdate],
});

for await (const message of query({
  prompt: "Ticket T-4521 for user_123 is resolved. Let the customer know.",
  options: {
    systemPrompt: "You are a support agent. Tell customers when their ticket changes status, then reply with the requestId.",
    mcpServers: { notifications },
    tools: [],
    allowedTools: ["mcp__notifications__send_ticket_update"],
  },
})) {
  if (message.type === "result" && message.subtype === "success") {
    console.log(message.result);
  }
}
```

```python Python lines theme={null}
import asyncio
import uuid
from typing import Any

from claude_agent_sdk import (
    ClaudeAgentOptions,
    ResultMessage,
    create_sdk_mcp_server,
    query,
    tool,
)
from courier import AsyncCourier

# Reads COURIER_API_KEY from the environment.
courier = AsyncCourier()


@tool(
    "send_ticket_update",
    "Tell a customer that their support ticket changed status.",
    {
        "type": "object",
        "properties": {
            "user_id": {"type": "string", "description": "The customer's Courier user ID"},
            "ticket_id": {"type": "string", "description": "The support ticket ID"},
            "status": {"type": "string", "enum": ["in_progress", "resolved"]},
        },
        "required": ["user_id", "ticket_id", "status"],
    },
)
async def send_ticket_update(args: dict[str, Any]) -> dict[str, Any]:
    response = await courier.send.message(
        message={
            "to": {"user_id": args["user_id"]},
            "template": "nt_01kx4h2jdafq8bk9aftxak4b40",
            "data": {"ticket_id": args["ticket_id"], "status": args["status"]},
        },
        idempotency_key=str(uuid.uuid4()),
    )
    return {"content": [{"type": "text", "text": f"Sent. requestId: {response.request_id}"}]}


notifications = create_sdk_mcp_server(
    name="notifications",
    version="1.0.0",
    tools=[send_ticket_update],
)


async def main() -> None:
    options = ClaudeAgentOptions(
        system_prompt="You are a support agent. Tell customers when their ticket changes status, then reply with the requestId.",
        mcp_servers={"notifications": notifications},
        tools=[],
        allowed_tools=["mcp__notifications__send_ticket_update"],
    )
    async for message in query(
        prompt="Ticket T-4521 for user_123 is resolved. Let the customer know.",
        options=options,
    ):
        if isinstance(message, ResultMessage) and message.subtype == "success":
            print(message.result)


if __name__ == "__main__":
    asyncio.run(main())
```

The SDK runs custom tools in an in-process MCP server, so the tool's full name is `mcp__notifications__send_ticket_update`. Listing it in `allowedTools` lets it run without a permission prompt. `tools: []` removes Claude Code's built-in tools, such as Bash and file edits, so this agent can only call your Courier tool.

The Courier SDK retries failed requests, and the `Idempotency-Key` makes those retries safe: a retried request returns the first response instead of sending twice. Use a fresh key for each send. A key built from the ticket and status would replay a failed first attempt, and silence a second notice with the same status, for 25 hours.

**2. Run it**

```bash npm theme={null}
npx tsx agent.ts
```

```bash pip theme={null}
python agent.py
```

The agent calls `send_ticket_update` and replies with the `requestId`. The template holds the content, so the agent picks the recipient and the moment without writing the message.

## Connect the Courier MCP server

**1. Point the agent at the server**

Pass the hosted server as an `http` MCP server, with your key in the `api_key` header. Save the file as `agent.ts` or `agent.py`.

```typescript TypeScript lines theme={null}
import { query } from "@anthropic-ai/claude-agent-sdk";

for await (const message of query({
  prompt: "Ticket T-4521 for user_123 is resolved. Let the customer know.",
  options: {
    systemPrompt:
      "You are a support agent. To notify a customer, send the published Courier template nt_01kx4h2jdafq8bk9aftxak4b40 with ticket_id and status in data, then reply with the requestId.",
    mcpServers: {
      courier: {
        type: "http",
        url: "https://mcp.courier.com",
        headers: { api_key: process.env.COURIER_API_KEY! },
      },
    },
    tools: [],
    allowedTools: ["mcp__courier__send_message_template"],
  },
})) {
  if (message.type === "result" && message.subtype === "success") {
    console.log(message.result);
  }
}
```

```python Python lines theme={null}
import asyncio
import os

from claude_agent_sdk import ClaudeAgentOptions, ResultMessage, query


async def main() -> None:
    options = ClaudeAgentOptions(
        system_prompt=(
            "You are a support agent. To notify a customer, send the published "
            "Courier template nt_01kx4h2jdafq8bk9aftxak4b40 with ticket_id and "
            "status in data, then reply with the requestId."
        ),
        mcp_servers={
            "courier": {
                "type": "http",
                "url": "https://mcp.courier.com",
                "headers": {"api_key": os.environ["COURIER_API_KEY"]},
            }
        },
        tools=[],
        allowed_tools=["mcp__courier__send_message_template"],
    )
    async for message in query(
        prompt="Ticket T-4521 for user_123 is resolved. Let the customer know.",
        options=options,
    ):
        if isinstance(message, ResultMessage) and message.subtype == "success":
            print(message.result)


if __name__ == "__main__":
    asyncio.run(main())
```

The system prompt names the template, and the agent sends it with `send_message_template`. Only the tools in `allowedTools` run without a permission prompt, so add Courier tools there, such as `mcp__courier__trace_message`, as the agent needs them.

The MCP send tools take a `user_id` or a `list_id`, not an email address, so the customer needs a saved profile.

**2. Run it**

```bash npm theme={null}
npx tsx agent.ts
```

```bash pip theme={null}
python agent.py
```

The agent calls `send_message_template` and replies with the `requestId`.

## Ask a person for approval

Send a template with an action button to Slack, Microsoft Teams, or the Inbox, and point the button at a URL in your app. The click reaches your app, not Courier, so record the decision there and resume the agent. The [action element
DOC](https://www.courier.com/docs/design/elemental/elements/action.md), [Slack
DOC](https://www.courier.com/docs/integrations/direct-message/slack.md), [Microsoft Teams
DOC](https://www.courier.com/docs/integrations/direct-message/microsoft-teams.md), and [Inbox
DOC](https://www.courier.com/docs/in-app/customize-the-inbox.md) pages cover each channel.

## Verify

**1. Find the request in Logs**

Copy the `requestId` from the agent's reply, open [Logs](https://app.courier.com/logs), and find that request. Logs are per environment, so a Test send appears only in Test logs.

**2. Confirm it delivered**

Expand the message timeline and confirm the provider accepted it. A channel with no configured provider shows as `UNROUTABLE`, so start with the inbox channel, which needs no setup.

## Next steps

* [Send from an AI agent
  GUIDE](https://www.courier.com/docs/guides/send-from-an-ai-agent.md) to test sends from Claude Code, Cursor, or Codex
* [The Courier MCP server
  DOC](https://www.courier.com/docs/resources/mcp.md) for every tool the server exposes
* [Send a message](https://www.courier.com/docs/api-reference/send/send-a-message.md) for every field a send accepts
* [Preferences
  DOC](https://www.courier.com/docs/recipients/preferences/overview.md), so a template mapped to a subscription topic respects each customer's opt-outs
