---
title: "Send an email"
description: "One message now or later, with attachments, or a hundred in one request."
url: "https://openemail.uk/docs/csharp/emails/send"
area: "C#"
category: "Guides"
---

# Send an email

One message now or later, with attachments, or a hundred in one request.

## Send a message

**send.cs**

```
var sent = await client.Emails.SendAsync(new Body
{
    ["from"] = "Acme Billing <billing@acme.com>",
    ["to"] = new[] { "ada@example.com", "grace@example.com" },
    ["replyTo"] = "support@acme.com",
    ["subject"] = "Your September invoice",
    ["html"] = "<p>The invoice is attached.</p>",
    ["text"] = "The invoice is attached.",
    ["tags"] = new Body { ["invoice"] = "inv_2026_09_4192" },
});

Console.WriteLine($"{sent["id"]} {sent["status"]}");
```

`to`, `cc` and `bcc` take one address or a list, and an address is text such as `Ada <ada@example.com>` or a dictionary with `email` and `name`. The body comes from `html` and `text`, from a stored `template`, or from a `draftId`.

Every send carries an idempotency key. The client makes one for each call and reuses it when it retries, so a retried request replays the first message. Pass `idempotencyKey:` to make that hold across processes and restarts.

## Attachments

**attachments.cs**

```
await using var invoice = File.OpenRead("invoice.pdf");

await client.Emails.SendAsync(new Body
{
    ["from"] = "billing@acme.com",
    ["to"] = "ada@example.com",
    ["subject"] = "Your September invoice",
    ["text"] = "The invoice is attached.",
    ["attachments"] = new[]
    {
        new Body { ["filename"] = "invoice.pdf", ["content"] = invoice, ["contentType"] = "application/pdf" },
        new Body { ["fileId"] = "file_3f9c2a7b1e4d8f60a5c7b92d" },
    },
});
```

`content` takes a `byte[]` or a readable `Stream`, which the client encodes as base64, or text that is base64 already. `fileId` names a file uploaded with `client.Files.UploadAsync()`, which is how a file larger than 5 MB is sent.

## Send later

**schedule.cs**

```
var scheduled = await client.Emails.SendAsync(new Body
{
    ["from"] = "billing@acme.com",
    ["to"] = "ada@example.com",
    ["subject"] = "Your invoice is due tomorrow",
    ["text"] = "A reminder that the invoice is due tomorrow.",
    ["scheduledAt"] = DateTimeOffset.UtcNow.AddDays(1),
});

var id = scheduled["id"]!.GetValue<string>();

await client.Emails.RescheduleAsync(id, DateTimeOffset.UtcNow.AddDays(2));

var cancelled = await client.Emails.CancelAsync(id);

Console.WriteLine(cancelled["status"]);
```

`scheduledAt` takes a `DateTimeOffset`, an ISO 8601 instant or a duration such as `PT1H`. A `DateTimeOffset` anywhere in a body is sent as an ISO 8601 instant in UTC. A scheduled message can be changed or cancelled until it goes.

## Send a batch

**batch.cs**

```
var result = await client.Emails.SendBatchAsync(new[]
{
    new Body { ["from"] = "dispatch@acme.com", ["to"] = "ada@example.com", ["subject"] = "Order AC-4192 has shipped", ["text"] = "It is on its way." },
    new Body { ["from"] = "dispatch@acme.com", ["to"] = "grace@example.com", ["subject"] = "Order AC-4193 has shipped", ["text"] = "It is on its way." },
});

Console.WriteLine($"{result.Sent} sent, {result.Failed} failed");

foreach (var item in result)
{
    if ((string?)item["status"] == "error")
    {
        Console.WriteLine($"{item["index"]} {item["error"]?["message"]}");
    }
}
```

A batch holds up to 100 messages and is never all or nothing: a bad address fails its own item and the rest still go. Check `Failed` and the `status` of each item instead of relying on an exception.
