Skip to content

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#

Before you start

You need an API key (sk_live_…, or sk_test_… to try it without delivering anything) and a sending domain you have verified in PostStack. See Authentication and Domains.

  1. Install the SDK

    bash
    go get github.com/getpoststack/go-sdk
  2. Initialize the client

    go
    import (
        "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"))
  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
    package 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
    }

Good to know

  • Official Go SDK — stdlib-only, zero external dependencies
  • Requires Go 1.22+

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.

Get started free

Next steps

Was this page helpful?