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 sending through PostStack.

go
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.