Email Fast Get a sandbox key
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

Next

See it for yourself

Sandbox keys run the real pipeline dry — real validation, real events, a hosted inbox, no email sent. Early access is onboarding now.