# Send email with Go

> Send email from Go with Email Fast: net/http and encoding/json against the REST API, with context timeouts, an idempotency key, and typed error handling.

Canonical: https://emailfast.dev/send-email-with/go

Go wants explicit contexts and explicit errors, and the Email Fast API obliges: one
`net/http` POST, a context deadline, a typed response. The standard library is the
whole integration — though the [Go SDK quickstart](/docs/quickstart-go) exists when
you want a generated client.

```go
// main.go — stdlib only
package main

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"os"
	"time"
)

type sendRequest struct {
	To             string            `json:"to"`
	Subject        string            `json:"subject"`
	HTML           string            `json:"html"`
	Data           map[string]string `json:"data,omitempty"`
	IdempotencyKey string            `json:"idempotency_key"`
}

type sendResponse struct {
	ID     string `json:"id"`
	Status string `json:"status"`
}

func sendEmail(ctx context.Context, key string, r sendRequest) (*sendResponse, error) {
	body, err := json.Marshal(r)
	if err != nil {
		return nil, err
	}
	req, err := http.NewRequestWithContext(ctx, http.MethodPost,
		"https://api.emailfast.dev/v1/emails", bytes.NewReader(body))
	if err != nil {
		return nil, err
	}
	req.Header.Set("Authorization", "Bearer "+key)
	req.Header.Set("Content-Type", "application/json")

	res, err := http.DefaultClient.Do(req)
	if err != nil {
		return nil, fmt.Errorf("send: %w", err)
	}
	defer res.Body.Close()

	if res.StatusCode >= 400 {
		b, _ := io.ReadAll(res.Body)
		return nil, fmt.Errorf("send rejected (%d): %s", res.StatusCode, b)
	}
	var out sendResponse
	if err := json.NewDecoder(res.Body).Decode(&out); err != nil {
		return nil, err
	}
	return &out, nil
}

func main() {
	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
	defer cancel()

	res, err := sendEmail(ctx, os.Getenv("EMAILFAST_API_KEY"), sendRequest{ // ef_sandbox_… while testing
		To:             "ada@example.com",
		Subject:        "Welcome",
		HTML:           "<h1>Hi {{name}}</h1>",
		Data:           map[string]string{"name": "Ada"},
		IdempotencyKey: "welcome-ada-1", // a retry replays, never re-sends
	})
	if err != nil {
		fmt.Fprintln(os.Stderr, err)
		os.Exit(1)
	}
	fmt.Println(res.ID, res.Status) // msg_... queued
}
```

a 202 from the API means the send is committed to a durable, partitioned outbox before we answer — a crash can't lose it, and a retry with the same idempotency key can't double-send.

## Gotchas

- `http.DefaultClient` has no timeout of its own — the context deadline is the only thing bounding this call. Always `NewRequestWithContext`.
- A 4xx from the API is not an `error` from `Do` — check `StatusCode` yourself or you'll decode an error body as success.
- Share one client; a new `http.Client` per send defeats connection pooling.
- Retrying with backoff is safe precisely because the idempotency key replays the original admission instead of creating a second send.

## Next

- [Go SDK quickstart](/docs/quickstart-go) — the generated client
- [The email API](/features/email-api) — batch, scheduling, message timelines
- [The raw REST quickstart](/docs/quickstart-curl)
- [All docs](/docs)
