Quickstart: Go
The standard library is enough: net/http to call, encoding/json to marshal, nothing to vendor.
Facts verified 2026-07-17 — corrections: hello@emailfast.dev
1. Send
The API is plain JSON over HTTPS with bearer auth. This is a complete program. (A generated Go SDK covers the same surface — see the docs — but the standard library hides nothing.)
package main
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"os"
)
func main() {
payload, _ := json.Marshal(map[string]any{
"to": "ada@example.com",
"subject": "Welcome to the ledger",
"html": "<h1>Hi {{name}}</h1><p>Your account is ready.</p>",
"data": map[string]string{"name": "Ada"},
"idempotency_key": "welcome-ada-1", // retry-safe: same key can never double-send
})
req, err := http.NewRequest("POST", "https://api.emailfast.dev/v1/emails", bytes.NewReader(payload))
if err != nil {
panic(err)
}
req.Header.Set("Authorization", "Bearer "+os.Getenv("EMAILFAST_API_KEY")) // ef_sandbox_… works everywhere
req.Header.Set("Content-Type", "application/json")
res, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer res.Body.Close()
var msg struct {
ID string `json:"id"`
Status string `json:"status"`
}
if err := json.NewDecoder(res.Body).Decode(&msg); err != nil {
panic(err)
}
fmt.Println(msg.ID, msg.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. That is what idempotency_key buys you: wrap the call in any retry loop and a timeout after the server committed the send still can't produce a duplicate.
2. Watch it happen
req, _ := http.NewRequest("GET", "https://api.emailfast.dev/v1/messages/"+msg.ID, nil)
req.Header.Set("Authorization", "Bearer "+os.Getenv("EMAILFAST_API_KEY"))
res, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer res.Body.Close()
var timeline struct {
Events []struct {
Type string `json:"type"`
} `json:"events"`
}
json.NewDecoder(res.Body).Decode(&timeline)
for _, e := range timeline.Events {
fmt.Println(e.Type) // admitted → rendered → (sandbox-)delivered …
}With a sandbox key the message lands in your hosted capture inbox — same pipeline, nothing sent: sandbox keys (ef_sandbox_…) that run the real pipeline dry: real validation, real rendering, real events, a hosted capture inbox — and no email leaves.