Send email with Go

Standard library only: net/http and encoding/json.

You need a verified sending domain and an API key from Settings › API Keys. If neither exists yet, the quickstart covers both in a couple of minutes.

Send

package main
 
import (
	"bytes"
	"encoding/json"
	"net/http"
	"os"
	"time"
)
 
type email struct {
	From           string `json:"from"`
	To             string `json:"to"`
	Subject        string `json:"subject"`
	HTML           string `json:"html"`
	IdempotencyKey string `json:"idempotencyKey"`
}
 
func send(e email) (*http.Response, error) {
	body, err := json.Marshal(e)
	if err != nil {
		return nil, err
	}
	req, err := http.NewRequest("POST", "https://sending.dev/api/v1/emails", bytes.NewReader(body))
	if err != nil {
		return nil, err
	}
	req.Header.Set("Authorization", "Bearer "+os.Getenv("SENDING_API_KEY"))
	req.Header.Set("Content-Type", "application/json")
 
	client := &http.Client{Timeout: 10 * time.Second}
	return client.Do(req)
}

A successful call answers 202 with the message id: accepted and queued, not yet delivered. Delivery, opens, bounces and complaints arrive later, on webhooks or in the dashboard.

What changes in Go

The json tags matter: the API reads idempotencyKey in camelCase, and Go would otherwise marshal the field as IdempotencyKey.

The thing that catches people

http.DefaultClient has no timeout. A request that hangs holds the goroutine forever, which is how a send path quietly becomes a leak under load.

Next

  • All the fields: cc and bcc, reply-to, attachments, templates, scheduling.
  • Webhooks: delivery, bounce and complaint events, signed with HMAC.
  • Domains: SPF, DKIM and MAIL FROM, and why sending is refused until they are in place.
  • MCP: the same operations as tools, when the one writing the code is an agent.

Nearby: Compiled languages

All 30 stacks