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

> Send email, SMS, push, and in-app notifications from .NET in five steps.

Courier is one API for every channel your product notifies through: email, SMS, push, Slack, Microsoft Teams, and an in-app inbox. You send once; Courier renders the template, picks the channel from the user's preferences and your routing, and delivers through the providers you already use. This guide gets a .NET app from zero to a delivered notification, then adds channel routing and a multi-step journey. About five minutes.

**What you need**

* A Courier account. [Start free](https://app.courier.com/signup).
* A Test API key from [Settings → API Keys](https://app.courier.com/settings/api-keys). Set it as `COURIER_API_KEY`.
* A user in Courier with an email or phone number. Step 2 creates one through the API.

**Building with an AI agent?** Install the Courier skill and your agent knows the API, the channels, and the patterns on this page.

```bash theme={null}
npx skills add trycourier/courier-skills
```

Setup for Claude Code, Cursor, and Codex, plus the hosted MCP server, is in [Build with AI](/docs/tools/ai-onboarding).

## 1. Install the SDK

```bash theme={null}
dotnet add package TryCourier
```

The package is a typed client for the whole API. It targets .NET 8 and .NET Standard 2.0, every method is async, and responses come back as typed models. It reads `COURIER_API_KEY` from the environment, so there is nothing to configure for the first send.

## 2. Create a user

Courier is built around users, not addresses. A user profile holds the email, phone number, push tokens, and chat handles for one person, plus their notification preferences. `Create` merges what you send and leaves the keys you omit alone; `Replace` overwrites the whole profile. Once it exists, every send names the user and Courier works out where to reach them.

```csharp theme={null}
using System.Collections.Generic;
using System.Text.Json;
using TryCourier;
using TryCourier.Models;
using TryCourier.Models.Journeys;
using TryCourier.Models.Profiles;
using TryCourier.Models.Send;

CourierClient client = new();

await client.Profiles.Create("user_123", new ProfileCreateParams
{
    Profile = new Dictionary<string, JsonElement>
    {
        { "email", JsonSerializer.SerializeToElement("ada@example.com") },
        { "phone_number", JsonSerializer.SerializeToElement("+15555550123") },
    },
});
```

Those using directives cover every snippet on this page.

## 3. Send a notification

A send names a template and a user, and passes the data specific to this event. The template lives in Design Studio, where it holds the content for every channel and the routing rules, so a copy change or a new channel never needs a deploy. `IdempotencyKey` is a property on the params object rather than a separate argument, and it means a retried request returns the original response instead of sending twice.

```csharp theme={null}
var response = await client.Send.Message(new SendMessageParams
{
    Message = new()
    {
        Template = "order-confirmation",
        To = new UserRecipient { UserID = "user_123" },
        Data = new Dictionary<string, JsonElement>
        {
            { "order_id", JsonSerializer.SerializeToElement("ORD-456") },
            { "total", JsonSerializer.SerializeToElement("$99.99") },
        },
    },
    IdempotencyKey = "order-confirmed-ORD-456",
});

Console.WriteLine(response.RequestID);
```

The call returns a `RequestID`. Open [Message Logs](https://app.courier.com/logs) and you will see the request, the channel Courier chose, the provider it used, and the delivery status as it updates.

## 4. Route across channels

Routing normally lives in the template, but a send can override it when the code knows something the template does not. `Single` tries channels in order and stops at the first that delivers; `All` sends on every listed channel. The user's preferences still apply on top, so a user who has opted out of SMS gets the email, and a user with no phone number on file does too.

```csharp theme={null}
await client.Send.Message(new SendMessageParams
{
    Message = new()
    {
        Template = "password-reset",
        To = new UserRecipient { UserID = "user_123" },
        Routing = new()
        {
            Method = TryCourier.Models.Send.Method.Single,
            Channels = ["sms", "email"],
        },
        Data = new Dictionary<string, JsonElement>
        {
            { "reset_url", JsonSerializer.SerializeToElement("https://app.example.com/reset/abc") },
        },
    },
});
```

`Method` is declared in more than one namespace in the SDK, so qualify it as above when you have imported both `TryCourier.Models` and `TryCourier.Models.Send`.

## 5. Start a journey

Some notifications are sequences: a welcome email now, a reminder tomorrow if setup is not finished, a different path for team plans. A journey is that sequence built in a visual editor as steps that send, wait, branch on user data, or digest a burst of events into one message. You publish it once, and your code only has to start it. When the sequence changes, the editor changes; the `Invoke` call does not.

```csharp theme={null}
var invoked = await client.Journeys.Invoke("new-signup-onboarding", new JourneyInvokeParams
{
    UserID = "user_123",
    Data = new Dictionary<string, JsonElement>
    {
        { "plan", JsonSerializer.SerializeToElement("team") },
    },
});

var run = await client.Journeys.Runs.Retrieve(invoked.RunID);
Console.WriteLine(run.Run.Status); // PROCESSING, WAITING, PROCESSED, CANCELED, ERROR, THROTTLED, NOT PROCESSED
```

`Invoke` returns before any message is sent, and `Status` comes back as a string, not an enum, because new values have been added before.

## Confirm delivery

A request fans out to one message per recipient and channel, and each message moves through its own lifecycle: enqueued, sent to the provider, delivered, opened, clicked. Look a message up by ID to read where it is.

```csharp theme={null}
var message = await client.Messages.Retrieve(messageID);
Console.WriteLine(message.Status); // ENQUEUED, SENT, DELIVERED, OPENED, CLICKED, UNDELIVERABLE
```

For production, subscribe to an [outbound webhook](/docs/platform/workspaces/outbound-webhooks) instead of polling: Courier posts each status change to your endpoint as it happens.

## Next steps

* [Working example](https://github.com/trycourier/courier-samples/tree/main/server/csharp): this guide as a runnable project.
* [Add an in-app inbox](/docs/platform/inbox/inbox-overview) with the React or web component SDK.
* [Let users set preferences](/docs/platform/preferences/preferences-overview).
* [Send API reference](/docs/api-reference/send/send-a-message): every field.
* [C# SDK reference](/docs/sdk-libraries/csharp).
