Send email with Go
Send transactional email from Go through PostStack.
The PostStack Go SDK is a stdlib-only client — no transitive dependencies, no surprise CVEs, and it builds into a tiny binary. Every method takes a context.Context so cancellation and timeout propagate from your HTTP handler down to the network call. The SDK is goroutine-safe: instantiate one *Client at startup and share it across handlers, workers, and goroutines.
Quickstart#
Install the SDK
bashgo get github.com/getpoststack/go-sdkInitialize the client
goimport ( "os" poststack "github.com/getpoststack/go-sdk" ) // One client per process — it is safe for concurrent use. var client = poststack.NewClient(os.Getenv("POSTSTACK_API_KEY"))Send an email
gopackage main import ( "context" "errors" "fmt" "log" "os" poststack "github.com/getpoststack/go-sdk" ) func main() { client := poststack.NewClient(os.Getenv("POSTSTACK_API_KEY")) result, err := client.Emails.Send(context.Background(), &poststack.SendEmailInput{ From: "hello@yourdomain.com", To: []string{"user@example.com"}, Subject: "Hello from Go!", Html: "<h1>Welcome!</h1>", }) if err != nil { var apiErr *poststack.Error if errors.As(err, &apiErr) { log.Fatalf("API error %d: %s", apiErr.StatusCode, apiErr.Message) } log.Fatal(err) } fmt.Println(result.ID) }Handle errors
gopackage main import ( "context" "errors" "log/slog" "time" poststack "github.com/getpoststack/go-sdk" ) // The client already retries 408, 5xx and short-Retry-After 429s (3 retries with // jittered backoff) and sends an Idempotency-Key, so a retried request is // never delivered twice. Only terminal errors reach this code. func send(ctx context.Context, client *poststack.Client, in *poststack.SendEmailInput) (*poststack.SendEmailResponse, error) { ctx, cancel := context.WithTimeout(ctx, 30*time.Second) defer cancel() result, err := client.Emails.Send(ctx, in) if err == nil { return result, nil } var apiErr *poststack.Error if errors.As(err, &apiErr) { // 400 invalid payload, 401/403 key problem, 422 unverified domain or // suppressed recipient; 429/5xx only after the client's own retries slog.Error("poststack send failed", "status", apiErr.StatusCode, "code", apiErr.Code, "request_id", apiErr.RequestID, "message", apiErr.Message, ) } return nil, err }
Framework integrations#
net/http
For pure stdlib HTTP servers, instantiate the client at startup and store it on a struct your handlers method into. Pass r.Context() through so client cancellation propagates to the PostStack call.
Gin / Echo / Chi
Inject the client via the framework’s context or DI helper. Gin: c.MustGet("poststack"). Echo: c.Get("poststack"). Chi: a middleware that attaches the client to r.Context(). Same Send(ctx, ...) call regardless of router.
Goroutine workers
For background sends, push send payloads onto a buffered channel and have N worker goroutines consume them. Each worker holds a reference to the shared *Client — goroutine-safe by design.
cron / scheduled jobs
robfig/cron jobs take no arguments, so create a bounded context inside the job — ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second); defer cancel() — and pass it to client.Emails.Send(ctx, ...) so a stuck request does not back up the scheduler.
Common pitfalls#
Using context.Background() everywhere
Pass the request or job context through instead. context.Background() cannot be cancelled, so a slow upstream call blocks the handler forever. Use r.Context() or context.WithTimeout.
Re-checking err with a wrong type assertion
Use errors.As(err, &apiErr) to extract *poststack.Error. A direct type assertion (err.(*poststack.Error)) panics on a non-API error.
Adding your own retry loop
The client already retries 408/429/5xx with backoff and reuses one idempotency key across attempts. Wrapping it in another retry loop multiplies attempts; tune it with the client options instead.
FAQ#
Is the Go SDK goroutine-safe?
Yes. Instantiate one *Client at startup and share it across all goroutines and handlers. The underlying http.Client is safe for concurrent use.
What Go versions are supported?
Go 1.22 and later, as declared in the module’s go.mod.
Are there any external dependencies?
No. The SDK is stdlib-only — only net/http, encoding/json, context, and friends. This minimises CVE exposure and keeps your final binary small.
Reference#
The docs this guide builds on:
- SDKs — every official client, its retries and idempotency keys
- SMTP relay — hosts, ports, TLS modes and the
X-PostStack-*headers - Emails API — every send field, attachments, scheduling and batch sends
- Templates — stored templates with variables
- Webhooks — event types, payloads and signature verification
- Suppressions — what happens to an address after a hard bounce or complaint
Ready to send from Go?
Create a free account and an API key — no card needed.