Send email from Go
Five steps: start a module, create a test key, send to the sandbox, read the log, then verify a domain so you can send to anyone.
1. Install
This guide needs no SDK. The API is plain JSON over HTTPS, so net/http and encoding/json from the standard library are all it takes.
mkdir avelto-quickstart && cd avelto-quickstartgo mod init example.com/avelto-quickstart2. Create an API key
Sign in, open API keys in the dashboard and create a test key. It starts with av_test_. Export it so the program can read it:
export AVELTO_API_KEY=av_test_...3. Send your first email
Every request carries the key as a bearer token. A rejected send comes back as a non-2xx status with a JSON body of { "error": { "code", "message" } }, decoded here into apiError.
// main.gopackage mainimport ( "bytes" "encoding/json" "fmt" "net/http" "os")const api = "https://api.avelto.dev"type apiError struct { Error struct { Code string `json:"code"` Message string `json:"message"` } `json:"error"`}func main() { body, _ := json.Marshal(map[string]string{ "from": "you@sandbox.avelto.dev", "to": "delivered@sandbox.avelto.dev", "subject": "Hello from Avelto", "text": "It works.", }) req, _ := http.NewRequest("POST", api+"/v1/emails", bytes.NewReader(body)) req.Header.Set("Authorization", "Bearer "+os.Getenv("AVELTO_API_KEY")) req.Header.Set("Content-Type", "application/json") res, err := http.DefaultClient.Do(req) if err != nil { panic(err) } defer res.Body.Close() if res.StatusCode != http.StatusCreated { var e apiError json.NewDecoder(res.Body).Decode(&e) fmt.Fprintf(os.Stderr, "%d %s: %s\n", res.StatusCode, e.Error.Code, e.Error.Message) os.Exit(1) } var out struct { ID string `json:"id"` } json.NewDecoder(res.Body).Decode(&out) fmt.Println(out.ID) // "9c1f4a52-6f6e-4b8f-9b8e-2e1a5c7d3f10"}go run .The API answers 201 Created with the email id:
HTTP/1.1 201 CreatedContent-Type: application/json{ "id": "9c1f4a52-6f6e-4b8f-9b8e-2e1a5c7d3f10" }4. Check the log
Add this function and call it with the id from step 3. status moves from queued to sent to delivered, and events records each step: email.queued, email.sent, email.delivered.
func getEmail(id string) { req, _ := http.NewRequest("GET", api+"/v1/emails/"+id, nil) req.Header.Set("Authorization", "Bearer "+os.Getenv("AVELTO_API_KEY")) res, err := http.DefaultClient.Do(req) if err != nil { panic(err) } defer res.Body.Close() var email struct { Status string `json:"status"` Events []struct { Type string `json:"type"` OccurredAt string `json:"occurred_at"` } `json:"events"` } json.NewDecoder(res.Body).Decode(&email) fmt.Println(email.Status) // "queued", then "sent", then "delivered" for _, e := range email.Events { fmt.Println(e.Type, e.OccurredAt) }}{ "id": "9c1f4a52-6f6e-4b8f-9b8e-2e1a5c7d3f10", "mode": "test", "from": "you@sandbox.avelto.dev", "to": ["delivered@sandbox.avelto.dev"], "subject": "Hello from Avelto", "status": "delivered", "events": [ { "id": "e1f0c3a4-8b2d-4c6e-9a1f-5d7b3e2c8a90", "type": "email.queued", "payload": {}, "occurred_at": "2026-09-17T10:12:04.000Z" }, { "id": "a7c2e9d1-3f4b-4a8e-b6c0-2d9e1f7b5c34", "type": "email.sent", "payload": { "test": true, "ses_message_id": "test-9c1f4a52-6f6e-4b8f-9b8e-2e1a5c7d3f10" }, "occurred_at": "2026-09-17T10:12:06.000Z" }, { "id": "c4b8d2f6-7e1a-4d3c-8f9b-6a2e0c5d1b78", "type": "email.delivered", "payload": { "test": true, "recipients": ["delivered@sandbox.avelto.dev"] }, "occurred_at": "2026-09-17T10:12:06.000Z" } ]}5. Verify a domain
Add a domain, publish the DNS records it prints (an ownership TXT, an SPF TXT and a DMARC TXT, then three DKIM CNAMEs once the ownership record is found), then poll GET /v1/domains/:id until status is verified. The GET re-checks DNS on every call. Use a subdomain such as mail.acme.com.
// Add "time" to the imports.func verifyDomain(name string) { body, _ := json.Marshal(map[string]string{"name": name}) req, _ := http.NewRequest("POST", api+"/v1/domains", bytes.NewReader(body)) req.Header.Set("Authorization", "Bearer "+os.Getenv("AVELTO_API_KEY")) req.Header.Set("Content-Type", "application/json") res, err := http.DefaultClient.Do(req) if err != nil { panic(err) } defer res.Body.Close() var domain struct { ID string `json:"id"` Status string `json:"status"` OwnershipVerified bool `json:"ownership_verified"` DNSRecords []struct { Type, Name, Value, Purpose string } `json:"dns_records"` } json.NewDecoder(res.Body).Decode(&domain) show := func() { for _, r := range domain.DNSRecords { fmt.Printf("%s\t%s\t%s\t(%s)\n", r.Type, r.Name, r.Value, r.Purpose) } } show() // the ownership record first, with SPF and DMARC // Publish the records, then poll. GET re-checks DNS on every call. // Once the ownership record is found, the DKIM records appear: publish those too. proven := domain.OwnershipVerified for domain.Status == "pending" { time.Sleep(30 * time.Second) req, _ := http.NewRequest("GET", api+"/v1/domains/"+domain.ID, nil) req.Header.Set("Authorization", "Bearer "+os.Getenv("AVELTO_API_KEY")) res, err := http.DefaultClient.Do(req) if err != nil { panic(err) } json.NewDecoder(res.Body).Decode(&domain) res.Body.Close() if domain.OwnershipVerified && !proven { show() } proven = domain.OwnershipVerified } fmt.Println(domain.Status) // "verified" or "failed"}Once the domain is verified, switch AVELTO_API_KEY to a live key (av_live_) and change from to an address on it, such as hello@mail.acme.com. Nothing else changes.
Next
- Send email: every field, attachments, tags, scheduling and idempotency.
- Webhooks: get events pushed to your app.
- Test mode: test keys, the sandbox sender and the simulator addresses.