> ## 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 OpenAI Agents SDK

> Let an AI agent built on the OpenAI Agents SDK notify customers through Courier, with a function 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 the agent sees.

> [!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 OpenAI API key](https://platform.openai.com/docs/quickstart#create-and-export-an-api-key)
* Node.js 20 or later, or Python 3.10 or later

## Set up the project

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

```bash npm theme={null}
npm init -y && npm pkg set type=module
npm install @openai/agents @trycourier/courier zod
npm install --save-dev tsx
```

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

Set both keys in the shell that runs the agent. Each SDK reads its key from the environment:

```bash theme={null}
export OPENAI_API_KEY="YOUR_OPENAI_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 SDK validates every call against the schema, so the agent can't send a status other than `in_progress` or `resolved`. Save the file as `agent.ts` or `agent.py`.

```typescript TypeScript lines theme={null}
import { Agent, run, tool } from "@openai/agents";
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({
  name: "send_ticket_update",
  description: "Tell a customer that their support ticket changed status.",
  parameters: z.object({
    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 execute({ 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 `Sent. requestId: ${requestId}`;
  },
});

const agent = new Agent({
  name: "Support agent",
  instructions: "You are a support agent. Tell customers when their ticket changes status, then reply with the requestId.",
  tools: [sendTicketUpdate],
});

const result = await run(agent, "Ticket T-4521 for user_123 is resolved. Let the customer know.");
console.log(result.finalOutput);
```

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

from agents import Agent, Runner
from agents.decorators import tool
from courier import AsyncCourier

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


@tool
async def send_ticket_update(
    user_id: str, ticket_id: str, status: Literal["in_progress", "resolved"]
) -> str:
    """Tell a customer that their support ticket changed status.

    Args:
        user_id: The customer's Courier user ID.
        ticket_id: The support ticket ID.
        status: The ticket's new status.
    """
    response = await courier.send.message(
        message={
            "to": {"user_id": user_id},
            "template": "nt_01kx4h2jdafq8bk9aftxak4b40",
            "data": {"ticket_id": ticket_id, "status": status},
        },
        idempotency_key=str(uuid.uuid4()),
    )
    return f"Sent. requestId: {response.request_id}"


agent = Agent(
    name="Support agent",
    instructions="You are a support agent. Tell customers when their ticket changes status, then reply with the requestId.",
    tools=[send_ticket_update],
)


async def main() -> None:
    result = await Runner.run(
        agent, "Ticket T-4521 for user_123 is resolved. Let the customer know."
    )
    print(result.final_output)


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

In TypeScript, the Zod schema is the contract. In Python, the signature and docstring are: `Literal` becomes an enum, and each line under `Args:` becomes a field description.

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**

Connect to the hosted server over Streamable HTTP, with your key in the `api_key` header. The connection runs from your process, and a tool filter picks which Courier tools the agent sees. Save the file as `agent.ts` or `agent.py`.

```typescript TypeScript lines theme={null}
import { Agent, createMCPToolStaticFilter, MCPServerStreamableHttp, run } from "@openai/agents";

const courierMcp = new MCPServerStreamableHttp({
  name: "courier",
  url: "https://mcp.courier.com",
  requestInit: { headers: { api_key: process.env.COURIER_API_KEY! } },
  toolFilter: createMCPToolStaticFilter({
    allowed: ["send_message_template", "trace_message"],
  }),
});

const agent = new Agent({
  name: "Support agent",
  instructions:
    "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: [courierMcp],
});

try {
  await courierMcp.connect();
  const result = await run(agent, "Ticket T-4521 for user_123 is resolved. Let the customer know.");
  console.log(result.finalOutput);
} finally {
  await courierMcp.close();
}
```

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

from agents import Agent, Runner
from agents.mcp import MCPServerStreamableHttp, create_static_tool_filter


async def main() -> None:
    async with MCPServerStreamableHttp(
        name="courier",
        params={
            "url": "https://mcp.courier.com",
            "headers": {"api_key": os.environ["COURIER_API_KEY"]},
        },
        tool_filter=create_static_tool_filter(
            allowed_tool_names=["send_message_template", "trace_message"]
        ),
    ) as courier_mcp:
        agent = Agent(
            name="Support agent",
            instructions=(
                "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_mcp],
        )
        result = await Runner.run(
            agent, "Ticket T-4521 for user_123 is resolved. Let the customer know."
        )
        print(result.final_output)


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

The filter exposes two tools: `send_message_template` to send, and `trace_message` to check delivery. Add names to the list to give the agent more of the API, or remove the filter to expose every tool.

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
