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

# Send an email

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

## Send a message

**send.go**

```
sent, err := client.Emails.Send(ctx, openemail.Body{
	"from":    "Acme Billing <billing@acme.com>",
	"to":      []string{"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":    openemail.Body{"invoice": "inv_2026_09_4192"},
})
if err != nil {
	return err
}

fmt.Println(sent.ID(), sent.String("status"))
```

`to`, `cc` and `bcc` take one address or a list, and an address is text such as `Ada <ada@example.com>` or a map 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 `openemail.WithIdempotencyKey` to make that hold across processes and restarts.

## Attachments

**attachments.go**

```
invoice, err := os.ReadFile("invoice.pdf")
if err != nil {
	return err
}

_, err = client.Emails.Send(ctx, openemail.Body{
	"from":    "billing@acme.com",
	"to":      "ada@example.com",
	"subject": "Your September invoice",
	"text":    "The invoice is attached.",
	"attachments": []openemail.Body{
		{"filename": "invoice.pdf", "content": invoice, "contentType": "application/pdf"},
		{"fileId": "file_3f9c2a7b1e4d8f60a5c7b92d"},
	},
})

return err
```

`content` takes a `[]byte` or an `io.Reader`, which the client encodes as base64, or text that is base64 already. `fileId` names a file uploaded with `client.Files.Upload`, which is how a file larger than 5 MB is sent.

## Send later

**schedule.go**

```
scheduled, err := client.Emails.Send(ctx, openemail.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": time.Now().Add(24 * time.Hour),
})
if err != nil {
	return err
}

if _, err := client.Emails.Reschedule(ctx, scheduled.ID(), time.Now().Add(48*time.Hour)); err != nil {
	return err
}

cancelled, err := client.Emails.Cancel(ctx, scheduled.ID())
if err != nil {
	return err
}

fmt.Println(cancelled.String("status"))
```

`scheduledAt` takes a `time.Time`, an ISO 8601 instant or a duration such as `PT1H`. A `time.Time` 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.go**

```
result, err := client.Emails.SendBatch(ctx, []openemail.Body{
	{"from": "dispatch@acme.com", "to": "ada@example.com", "subject": "Order AC-4192 has shipped", "text": "It is on its way."},
	{"from": "dispatch@acme.com", "to": "grace@example.com", "subject": "Order AC-4193 has shipped", "text": "It is on its way."},
})
if err != nil {
	return err
}

fmt.Println(result.Sent, "sent,", result.Failed, "failed")

for _, item := range result.Items {
	if item.String("status") == "error" {
		fmt.Println(item.Int("index"), item.Object("error").String("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 error.
