Guides
Send email with Go
net/http, a context deadline, and errors that tell you why — no SDK required.
Facts verified 2026-07-17 — corrections: hello@emailfast.dev
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 exists when you want a generated client.
// 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.DefaultClienthas no timeout of its own — the context deadline is the only thing bounding this call. AlwaysNewRequestWithContext.- A 4xx from the API is not an
errorfromDo— checkStatusCodeyourself or you'll decode an error body as success. - Share one client; a new
http.Clientper 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 — the generated client
- The email API — batch, scheduling, message timelines
- The raw REST quickstart
- All docs