Skip to content

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

bash
go get github.com/getpoststack/go-sdk

2. Initialize the client

go
import (
    "context"

    poststack "github.com/getpoststack/go-sdk"
)

client := poststack.NewClient("sk_live_...")

3. Send an email

go
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 calling the PostStack API.

go
package main

import (
    "context"
    "errors"
    "log/slog"
    "time"

    poststack "github.com/getpoststack/go-sdk"
)

func sendWithRetry(ctx context.Context, client *poststack.Client, in *poststack.SendEmailInput) (*poststack.Email, error) {
    var lastErr error
    for attempt := 0; attempt < 3; attempt++ {
        result, err := client.Emails.Send(ctx, in)
        if err == nil {
            return result, nil
        }
        var apiErr *poststack.Error
        if errors.As(err, &apiErr) {
            if apiErr.StatusCode == 429 {
                // Honour Retry-After
                time.Sleep(apiErr.RetryAfter)
                continue
            }
            if apiErr.StatusCode >= 500 {
                time.Sleep(time.Duration(1<<attempt) * time.Second)
                continue
            }
            // 4xx — log and bail
            slog.Error("poststack 4xx",
                "status", apiErr.StatusCode,
                "request_id", apiErr.RequestID,
                "message", apiErr.Message,
            )
        }
        return nil, err
    }
    return nil, lastErr
}

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.

  • Logging the API key

    `%+v` on the client struct prints the API key. Strip secrets before logging — the SDK provides `String()` that redacts the key.

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. The SDK uses range-over-func and slog from the stdlib, so older versions are not supported.

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.