SDKs

Go

Use the official Lettermint Go SDK to send email from a Go application. The module provides typed requests and responses for the Sending API and Team API. See Integrations for the SDK capability overview.

This guide covers SDK v3, which uses the module path github.com/lettermint/lettermint-go/v3. If your project uses v2, follow the Go SDK v3 upgrade guide on GitHub.

Requirements

Before you start you need:

1. Installation

Install the SDK using Go modules:

TerminalCode
go get github.com/lettermint/lettermint-go/v3

Import it with the /v3 path:

GoCode
import lettermint "github.com/lettermint/lettermint-go/v3"

2. Send your first email

Create the client with your Project API token as the sending token:

GoCode
package main import ( "context" "fmt" "log" "os" lettermint "github.com/lettermint/lettermint-go/v3" ) func main() { client, err := lettermint.New( lettermint.WithSendingToken(os.Getenv("LETTERMINT_PROJECT_TOKEN")), ) if err != nil { log.Fatal(err) } ctx := context.Background() resp, err := client.Emails.Compose(). From("John Doe <john@yourdomain.com>"). To("recipient@example.com"). Subject("Hello from Lettermint"). Text("This is a test email sent using the Lettermint Go SDK."). Send(ctx) if err != nil { log.Fatal(err) } fmt.Printf("Email sent with ID: %s\n", resp.MessageID) }

The client stores nothing about a message, so create it once and share it across goroutines. Every method takes a context.Context first. You can also pass the token as a string with lettermint.NewFromToken(token).

client.Emails.Send takes the message in the API's format, lettermint.SendMailRequest. Optional fields such as HTML and Text are lettermint.Nullable values, set with lettermint.Value(...):

GoCode
resp, err := client.Emails.Send(ctx, lettermint.SendMailRequest{ From: "John Doe <john@yourdomain.com>", To: []string{"recipient@example.com"}, Subject: "Hello from Lettermint", Text: lettermint.Value("This is a test email sent using the Lettermint Go SDK."), })

The Team API uses a separate Team API token. Pass it with lettermint.WithTeamToken, alone or next to lettermint.WithSendingToken. Each part of the client uses its own token and never falls back to the other. See the Team API quickstart.

3. Email features

The examples in this section use client.Emails.Compose(), a builder. The builder is a value: every setter returns a new builder and leaves the original unchanged, so one builder is safe to share and reuse. Keep the value that a setter returns when you build an email over several statements:

GoCode
draft := client.Emails.Compose(). From("John Doe <john@yourdomain.com>"). To("recipient@example.com"). Subject("Your invoice") if copyAccountant { draft = draft.CC("accountant@yourdomain.com") } _, err := draft.HTML("<p>Your invoice is ready.</p>").Send(ctx)

Each builder example is equivalent to a lettermint.SendMailRequest passed to client.Emails.Send. Use whichever reads better.

Basic email

Send a simple text or HTML email:

GoCode
resp, err := client.Emails.Compose(). From("John Doe <john@yourdomain.com>"). To("recipient@example.com"). Subject("Your account is ready!"). HTML("<h1>Welcome!</h1><p>Thanks for signing up.</p>"). Text("Welcome! Thanks for signing up."). Send(ctx)

Multiple recipients

Send to multiple recipients using CC and BCC. Each of To, CC, BCC, and ReplyTo replaces its list:

GoCode
resp, err := client.Emails.Compose(). From("John Doe <john@yourdomain.com>"). To("user1@example.com", "user2@example.com"). CC("manager@yourdomain.com"). BCC("archive@yourdomain.com"). Subject("Monthly Newsletter"). HTML("<h1>This Month's Updates</h1>"). Send(ctx)

Custom headers and reply-to

Add custom headers and set reply-to addresses:

GoCode
resp, err := client.Emails.Compose(). From("support@yourdomain.com"). To("customer@example.com"). ReplyTo("help@yourdomain.com"). Subject("Support Ticket #12345"). Headers(map[string]string{ "X-Priority": "1", "X-Ticket-ID": "12345", }). HTML("<p>Your support ticket has been updated.</p>"). Send(ctx)

Metadata

Add metadata for tracking and webhook payloads:

GoCode
resp, err := client.Emails.Compose(). From("notifications@yourdomain.com"). To("user@example.com"). Subject("Order Confirmation"). Metadata(map[string]string{ "order_id": "12345", "customer_id": "cust_789", "campaign": "order_confirmation", }). HTML("<p>Your order has been confirmed.</p>"). Send(ctx)

Metadata is included in webhook payloads but not added to the actual email headers. Use it for tracking and analytics purposes.

Tags

Categorize emails for filtering and analytics:

GoCode
resp, err := client.Emails.Compose(). From("alerts@yourdomain.com"). To("admin@example.com"). Subject("System Alert"). Tags( lettermint.MessageTagInput{Name: "category", Value: "system-alerts"}, lettermint.MessageTagInput{Name: "environment", Value: "production"}, ). HTML("<p>Critical system alert detected.</p>"). Send(ctx)

Tags accepts up to 20 name and value pairs. The SDK checks names and values before it sends the request and returns a *lettermint.ClientValidationError if one is invalid. Tag still sets the existing singular tag, which has no deprecation date. See Tags documentation.

A setter that gets invalid input records the error on the builder it returns. Send returns that error and sends nothing. Use Err() to check the builder earlier.

Route selection

Direct emails to specific routes within your project:

GoCode
resp, err := client.Emails.Compose(). From("notifications@yourdomain.com"). To("user@example.com"). Subject("Welcome!"). Route("transactional"). HTML("<p>Welcome to our platform.</p>"). Send(ctx)

File attachments

Attach files to your emails. Set Content to raw bytes and the SDK base64-encodes them, or set ContentBase64 to content that is already encoded:

GoCode
pdf, err := os.ReadFile("/path/to/document.pdf") if err != nil { log.Fatal(err) } resp, err := client.Emails.Compose(). From("invoices@yourdomain.com"). To("customer@example.com"). Subject("Your Invoice"). HTML("<p>Please find your invoice attached.</p>"). Attach(lettermint.Attachment{ Filename: "invoice.pdf", Content: pdf, ContentType: "application/pdf", }). Send(ctx)

Inline images

Embed images directly in your HTML using Content-ID. Set ContentID and reference it as cid: in the markup. See Inline images.

GoCode
logo, err := os.ReadFile("/path/to/logo.png") if err != nil { log.Fatal(err) } resp, err := client.Emails.Compose(). From("marketing@yourdomain.com"). To("customer@example.com"). Subject("Welcome to Our Platform"). HTML(`<p>Welcome!</p><img src="cid:logo" alt="Company Logo">`). Attach(lettermint.Attachment{Filename: "logo.png", Content: logo, ContentID: "logo"}). Send(ctx)

Idempotency

Pass an idempotency key so a retry never sends the same email twice. If a request with the same key is repeated within the idempotency window, Lettermint returns the original result instead of sending again. The key is an option of the call that sends the email:

GoCode
resp, err := client.Emails.Compose(). From("notifications@yourdomain.com"). To("user@example.com"). Subject("Order Confirmation"). HTML("<p>Your order has been confirmed.</p>"). Send(ctx, lettermint.WithIdempotencyKey("order-12345-confirmation"))

client.Emails.Send and client.Emails.SendBatch take the same option. The key applies only to that call, and the SDK never retries on its own.

Use one unique key for each logical email. For example, combine the order ID and the email type. A retry with the same key does not send a duplicate email. See the idempotency documentation for the full behaviour.

Batch sending

Send up to 500 emails in one request. Use Build to include a builder in the batch:

GoCode
base := client.Emails.Compose(). From("John Doe <john@yourdomain.com>"). Subject("Your weekly summary") second, err := base.To("user2@example.com").Text("Your summary is ready.").Build() if err != nil { log.Fatal(err) } results, err := client.Emails.SendBatch(ctx, []lettermint.SendMailRequest{ { From: "John Doe <john@yourdomain.com>", To: []string{"user1@example.com"}, Subject: "Hi", Text: lettermint.Value("Hello"), }, second, }, lettermint.WithIdempotencyKey("weekly-summary-2026-10-05"))

4. Client configuration

Customize the client with functional options:

GoCode
import "time" client, err := lettermint.New( lettermint.WithSendingToken(os.Getenv("LETTERMINT_PROJECT_TOKEN")), lettermint.WithTimeout(30*time.Second), lettermint.WithBaseURL("https://api.lettermint.co/v1"), )

The timeout covers the response headers and the body. lettermint.WithRequestTimeout(d) overrides it for one call, and cancelling the context.Context stops a request. lettermint.WithHTTPClient(c) takes your own http.Client, for example with a proxy. The SDK copies it and never changes yours. The SDK never follows redirects, so tokens never go to another host.

5. Response

GoCode
resp, err := client.Emails.Compose(). From("John Doe <john@yourdomain.com>"). To("recipient@example.com"). Subject("Test"). Text("Hello!"). Send(ctx) if err != nil { log.Fatal(err) } fmt.Println(resp.MessageID) // Unique email ID fmt.Println(resp.Status) // Current status

6. Error handling

Every error the SDK returns implements lettermint.Error. Check the types with errors.As:

GoCode
import "errors" resp, err := client.Emails.Send(ctx, message, lettermint.WithIdempotencyKey(key)) var validation *lettermint.ValidationError var rateLimit *lettermint.RateLimitError var timeout *lettermint.TimeoutError var apiErr *lettermint.APIError switch { case err == nil: fmt.Println(resp.MessageID) case errors.As(err, &validation): fmt.Println(validation.Message, validation.Errors) case errors.As(err, &rateLimit): // Wait for rateLimit.RetryAfter (when set), then retry with the same key. case errors.As(err, &timeout): // The outcome is unknown. Retry with the same key. case errors.As(err, &apiErr): fmt.Println(apiErr.Status, apiErr.Code, apiErr.Message) default: log.Fatal(err) }

*lettermint.APIError carries Status, Code, Message, Details, and Body. The status-specific types embed it and unwrap to it, so errors.As(err, &apiErr) also matches a *lettermint.RateLimitError. The status types are *AuthenticationError (401), *PermissionError (403), *NotFoundError (404), *ConflictError (409), *ValidationError (422), *RateLimitError (429), and *ServerError (5xx).

Other errors include *ConnectionError, *UnexpectedResponseError, *RedirectError, *ConfigError for a missing token, and *ClientValidationError when the SDK rejects a request before it sends it. A cancelled context returns the context's error, not an SDK error.

The SDK never retries on its own. Reuse the same idempotency key when you retry so the customer gets one email.

7. Webhook verification

Verify incoming webhooks from Lettermint with the signing secret of the webhook endpoint. Pass the raw request body: the signature covers the exact bytes, so decoding and re-encoding the JSON breaks it.

GoCode
package main import ( "errors" "log" "net/http" "os" lettermint "github.com/lettermint/lettermint-go/v3" ) func main() { webhook, err := lettermint.NewWebhook(os.Getenv("LETTERMINT_WEBHOOK_SECRET")) if err != nil { log.Fatal(err) } http.HandleFunc("POST /webhooks/lettermint", func(w http.ResponseWriter, r *http.Request) { // VerifyRequest reads the raw body and checks both signature headers. event, err := webhook.VerifyRequest(r) var verification *lettermint.WebhookVerificationError if errors.As(err, &verification) { log.Printf("rejected webhook: %s", verification.Reason) http.Error(w, "invalid signature", http.StatusBadRequest) return } if err != nil { http.Error(w, "error", http.StatusInternalServerError) return } switch event.Event { case lettermint.WebhookEventMessageDelivered: var data struct { MessageID string `json:"message_id"` Recipient string `json:"recipient"` } if err := event.DecodeData(&data); err == nil { log.Printf("Email %s delivered to %s", data.MessageID, data.Recipient) } case lettermint.WebhookEventMessageHardBounced: log.Printf("Hard bounce in event %s", event.ID) } w.WriteHeader(http.StatusNoContent) }) log.Fatal(http.ListenAndServe(":8080", nil)) }

VerifyRequest requires the X-Lettermint-Signature and X-Lettermint-Delivery headers and accepts deliveries signed within 5 minutes by default. Change that with lettermint.NewWebhook(secret, lettermint.WithTolerance(time.Minute)). A failure is a *lettermint.WebhookVerificationError with a Reason. event.Event is a lettermint.WebhookEvent, and unknown events pass through as their string, so give switch statements a default branch where it matters. See Webhook signatures for the signing scheme.

Store your webhook secret in an environment variable. Never hardcode secrets in your application.

Track delivery, opens, and bounces

Sending is only half of a transactional setup. To see what happens after a message leaves your app, combine the SDK with Lettermint's platform features:

  • Email tracking records opens and clicks, with bot filtering so your metrics stay accurate.
  • Webhooks push delivery, bounce, and complaint events to your server in real time, so you can update order records or suppress bad addresses.
  • Test addresses let you trigger a hard or soft bounce on demand while you build and verify your webhook handler.

Next steps

Tags

Organize and filter emails with tags.

Tracking

Track opens, clicks, and deliverability.

Webhooks

Receive delivery events through webhooks.

SMTP Alternative

Send via SMTP instead of the API.

GitHub Repository

Find the complete source code, report issues, or contribute on GitHub.