Send emails with Go
Learn how to send transactional emails using PostStack and Go.
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.
1. Install the SDK
go get github.com/getpoststack/go-sdk2. Initialize the client
import (
"context"
poststack "github.com/getpoststack/go-sdk"
)
client := poststack.NewClient("sk_live_...")3. Send an email
package 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)
}4. Handle errors
Go idioms for error handling, retries, and structured logging when sending through PostStack.
package main
import (
"context"
"errors"
"log/slog"
"time"
poststack "github.com/getpoststack/go-sdk"
)
// The client already retries 408, 429 and 5xx responses (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 receive a `context.Context` you can plumb directly into `client.Emails.Send(ctx, ...)`. Use `context.WithTimeout` to bound each send 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.
Notes
- Official Go SDK — stdlib-only, zero external dependencies
- Requires Go 1.22+
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.
Related guides
Ready to send emails with Go?
Create a free account and get your API key in under a minute.