# Quickstart: Go

> Send email from Go with the standard library: net/http and encoding/json against the Email Fast REST API, with a sandbox key and an idempotency key shown.

Canonical: https://emailfast.dev/docs/quickstart-go

## 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](/docs) — but the
standard library hides nothing.)

```go
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

```go
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

- [The raw REST reference](/docs/quickstart-curl)
- [Templates & MJML](/features/templates)
- [Migrating from SendGrid/Mailgun/Postmark](/features/migration)
