Email Fast Get a sandbox key
Docs

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.

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.