Skip to main content
This guide covers advanced features of the Lettr Go SDK including attachments, templates, batch sending, error handling, and best practices for production applications.
New to the Go SDK? Start with the Go Quickstart to learn the basics first.

Advanced Features

CC and BCC Recipients

Add CC and BCC recipients:
resp, err := client.Emails.Send(ctx, &lettr.SendEmailRequest{
    From:    "notifications@yourdomain.com",
    To:      []string{"primary@example.com"},
    Cc:      []string{"cc@example.com", "cc2@example.com"},
    Bcc:     []string{"bcc@example.com"},
    Subject: "Team notification",
    Html:    "<p>This email has CC and BCC recipients.</p>",
})
BCC recipients are hidden from all other recipients. They receive the email but their addresses are not visible in the headers.

Reply-To Address

Specify a different reply-to address:
resp, err := client.Emails.Send(ctx, &lettr.SendEmailRequest{
    From:    "notifications@yourdomain.com",
    ReplyTo: "support@yourdomain.com",
    To:      []string{"user@example.com"},
    Subject: "Support notification",
    Html:    "<p>Click reply to contact our support team.</p>",
})

Attachments

Add file attachments to your emails:
import (
    "encoding/base64"
    "os"
)

// Read file and encode to base64
fileData, err := os.ReadFile("invoice.pdf")
if err != nil {
    log.Fatal(err)
}
encodedContent := base64.StdEncoding.EncodeToString(fileData)

resp, err := client.Emails.Send(ctx, &lettr.SendEmailRequest{
    From:    "billing@yourdomain.com",
    To:      []string{"customer@example.com"},
    Subject: "Your invoice",
    Html:    "<p>Please find your invoice attached.</p>",
    Attachments: []lettr.Attachment{
        {
            Filename: "invoice.pdf",
            Content:  encodedContent,
            Type:     "application/pdf",
        },
    },
})
Attach multiple files:
attachments := []lettr.Attachment{}

// Attach PDF
pdfData, _ := os.ReadFile("document.pdf")
attachments = append(attachments, lettr.Attachment{
    Filename: "document.pdf",
    Content:  base64.StdEncoding.EncodeToString(pdfData),
    Type:     "application/pdf",
})

// Attach image
imgData, _ := os.ReadFile("logo.png")
attachments = append(attachments, lettr.Attachment{
    Filename: "logo.png",
    Content:  base64.StdEncoding.EncodeToString(imgData),
    Type:     "image/png",
})

resp, err := client.Emails.Send(ctx, &lettr.SendEmailRequest{
    From:        "sender@yourdomain.com",
    To:          []string{"recipient@example.com"},
    Subject:     "Files attached",
    Html:        "<p>See attached files.</p>",
    Attachments: attachments,
})
Attachments must be base64-encoded. The total size of all attachments should not exceed 10MB. Larger files should be hosted and linked instead.

Templates

Send emails using Lettr-managed templates:
resp, err := client.Emails.Send(ctx, &lettr.SendEmailRequest{
    From:       "notifications@yourdomain.com",
    To:         []string{"user@example.com"},
    TemplateID: "welcome-email",
    MergeTags: map[string]interface{}{
        "name":         "John Doe",
        "company":      "Acme Corp",
        "verify_url":   "https://example.com/verify/abc123",
        "support_email": "support@yourdomain.com",
    },
})
Templates are managed in the Lettr dashboard. Use merge tags to personalize content without rebuilding HTML in your code.

Custom Headers

Add custom email headers:
resp, err := client.Emails.Send(ctx, &lettr.SendEmailRequest{
    From:    "notifications@yourdomain.com",
    To:      []string{"user@example.com"},
    Subject: "Custom headers example",
    Html:    "<p>This email has custom headers.</p>",
    Headers: map[string]string{
        "X-Campaign-ID":   "summer-2024",
        "X-Priority":      "high",
        "X-Mailer":        "Acme Mailer v1.0",
    },
})

Tracking

Enable open and click tracking:
resp, err := client.Emails.Send(ctx, &lettr.SendEmailRequest{
    From:    "marketing@yourdomain.com",
    To:      []string{"user@example.com"},
    Subject: "Newsletter",
    Html:    "<p>Check out <a href='https://example.com'>our website</a>!</p>",
    Options: &lettr.EmailOptions{
        OpenTracking:  true,
        ClickTracking: true,
    },
})
Open tracking works by embedding a transparent pixel image. Click tracking rewrites links to go through Lettr’s tracking domain. Both features respect user privacy and comply with email regulations.

Metadata

Attach custom metadata for tracking and filtering:
resp, err := client.Emails.Send(ctx, &lettr.SendEmailRequest{
    From:    "notifications@yourdomain.com",
    To:      []string{"user@example.com"},
    Subject: "Order confirmation",
    Html:    "<p>Your order has been confirmed.</p>",
    Metadata: map[string]string{
        "user_id":    "12345",
        "order_id":   "ORD-98765",
        "campaign":   "abandoned-cart",
        "environment": "production",
    },
})
Metadata is returned in webhook events and can be used to correlate emails with your application data.

Error Handling

The SDK provides structured error responses for different error scenarios:
resp, err := client.Emails.Send(ctx, &lettr.SendEmailRequest{
    From:    "invalid@unverified-domain.com",
    To:      []string{"user@example.com"},
    Subject: "Test",
    Html:    "<p>Test</p>",
})

if err != nil {
    switch e := err.(type) {
    case *lettr.ValidationError:
        // 422 validation errors
        log.Printf("Validation failed: %v", e.Message)
        for field, messages := range e.Errors {
            log.Printf("  %s: %v", field, messages)
        }
    case *lettr.AuthError:
        // 401 authentication errors
        log.Printf("Authentication failed: %v", e.Message)
    case *lettr.RateLimitError:
        // 429 rate limit errors
        log.Printf("Rate limit exceeded: %v", e.Message)
    case *lettr.APIError:
        // Other API errors (500, 503, etc.)
        log.Printf("API error: %v", e.Message)
    default:
        // Network or other errors
        log.Printf("Request failed: %v", err)
    }
    return
}

Common Error Scenarios

Unverified domain:
  • Error: 422 Validation Error
  • Message: “The from address domain is not verified”
  • Solution: Verify your domain in the dashboard
Invalid API key:
  • Error: 401 Authentication Error
  • Message: “Invalid API key”
  • Solution: Check your API key is correct and active
Rate limit exceeded:
  • Error: 429 Rate Limit Error
  • Message: “Too many requests”
  • Solution: Implement exponential backoff retry logic

Batch Sending

Send multiple emails efficiently using goroutines:
package main

import (
    "context"
    "log"
    "sync"

    lettr "github.com/lettr-com/lettr-go"
)

func main() {
    client := lettr.NewClient(os.Getenv("LETTR_API_KEY"))
    ctx := context.Background()

    recipients := []string{
        "user1@example.com",
        "user2@example.com",
        "user3@example.com",
    }

    var wg sync.WaitGroup

    for _, recipient := range recipients {
        wg.Add(1)

        go func(to string) {
            defer wg.Done()

            resp, err := client.Emails.Send(ctx, &lettr.SendEmailRequest{
                From:    "notifications@yourdomain.com",
                To:      []string{to},
                Subject: "Batch notification",
                Html:    "<p>This is a batch email.</p>",
            })

            if err != nil {
                log.Printf("Failed to send to %s: %v", to, err)
                return
            }

            log.Printf("Sent to %s (Request ID: %s)", to, resp.Data.RequestID)
        }(recipient)
    }

    wg.Wait()
    log.Println("All emails sent")
}

With Rate Limiting

Use a semaphore to limit concurrent requests:
func sendBatch(client *lettr.Client, recipients []string, maxConcurrent int) {
    ctx := context.Background()
    sem := make(chan struct{}, maxConcurrent)
    var wg sync.WaitGroup

    for _, recipient := range recipients {
        wg.Add(1)

        go func(to string) {
            defer wg.Done()

            // Acquire semaphore
            sem <- struct{}{}
            defer func() { <-sem }()

            resp, err := client.Emails.Send(ctx, &lettr.SendEmailRequest{
                From:    "notifications@yourdomain.com",
                To:      []string{to},
                Subject: "Batch email",
                Html:    "<p>Hello!</p>",
            })

            if err != nil {
                log.Printf("Failed: %v", err)
                return
            }

            log.Printf("Sent to %s", to)
        }(recipient)
    }

    wg.Wait()
}

// Usage
sendBatch(client, recipients, 10) // Max 10 concurrent requests
For large batches, consider using a worker pool pattern or a job queue to manage concurrent requests and handle failures gracefully.

Context and Timeouts

All SDK methods accept context.Context for cancellation and timeouts:
import (
    "context"
    "time"
)

// Set a 10-second timeout
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()

resp, err := client.Emails.Send(ctx, &lettr.SendEmailRequest{
    From:    "sender@yourdomain.com",
    To:      []string{"user@example.com"},
    Subject: "Time-sensitive email",
    Html:    "<p>This request will timeout after 10 seconds.</p>",
})

if err != nil {
    if ctx.Err() == context.DeadlineExceeded {
        log.Println("Request timed out")
    } else {
        log.Printf("Request failed: %v", err)
    }
    return
}

Cancellation

Cancel requests when they’re no longer needed:
ctx, cancel := context.WithCancel(context.Background())

// Start request in goroutine
go func() {
    resp, err := client.Emails.Send(ctx, &lettr.SendEmailRequest{
        From:    "sender@yourdomain.com",
        To:      []string{"user@example.com"},
        Subject: "Cancellable email",
        Html:    "<p>This can be cancelled.</p>",
    })

    if err != nil {
        if ctx.Err() == context.Canceled {
            log.Println("Request was cancelled")
        }
    }
}()

// Cancel after 1 second
time.Sleep(1 * time.Second)
cancel()

Best Practices

Use Environment Variables

Never hardcode API keys. Use environment variables or a secrets manager:
apiKey := os.Getenv("LETTR_API_KEY")
if apiKey == "" {
    log.Fatal("LETTR_API_KEY is required")
}

client := lettr.NewClient(apiKey)

Validate Before Sending

Validate email addresses before making API calls to avoid unnecessary errors:
import "net/mail"

func isValidEmail(email string) bool {
    _, err := mail.ParseAddress(email)
    return err == nil
}

if !isValidEmail(recipient) {
    log.Printf("Invalid email address: %s", recipient)
    return
}

Log Request IDs

Always log the RequestID from successful sends for tracking and debugging:
resp, err := client.Emails.Send(ctx, req)
if err != nil {
    log.Printf("Send failed: %v", err)
    return
}

log.Printf("Email sent successfully - Request ID: %s, Accepted: %d",
    resp.Data.RequestID, resp.Data.Accepted)

Handle Errors Gracefully

Implement retry logic for transient errors:
import "time"

func sendWithRetry(client *lettr.Client, req *lettr.SendEmailRequest, maxRetries int) error {
    ctx := context.Background()

    for attempt := 0; attempt <= maxRetries; attempt++ {
        resp, err := client.Emails.Send(ctx, req)
        if err == nil {
            log.Printf("Email sent: %s", resp.Data.RequestID)
            return nil
        }

        // Don't retry validation errors
        if _, ok := err.(*lettr.ValidationError); ok {
            return err
        }

        // Retry on rate limit or server errors
        if attempt < maxRetries {
            backoff := time.Duration(attempt+1) * time.Second
            log.Printf("Attempt %d failed, retrying in %v: %v", attempt+1, backoff, err)
            time.Sleep(backoff)
            continue
        }

        return err
    }

    return nil
}

Use Structured Logging

Use structured logging for better observability:
import "log/slog"

logger := slog.Default()

resp, err := client.Emails.Send(ctx, req)
if err != nil {
    logger.Error("Failed to send email",
        "error", err,
        "to", req.To,
        "subject", req.Subject,
    )
    return
}

logger.Info("Email sent successfully",
    "request_id", resp.Data.RequestID,
    "accepted", resp.Data.Accepted,
    "to", req.To,
)

Reuse the Client

Create a single client instance and reuse it across requests:
// Good: Create once, reuse
var emailClient = lettr.NewClient(os.Getenv("LETTR_API_KEY"))

func sendEmail(to, subject, html string) error {
    resp, err := emailClient.Emails.Send(context.Background(), &lettr.SendEmailRequest{
        From:    "notifications@yourdomain.com",
        To:      []string{to},
        Subject: subject,
        Html:    html,
    })
    // ...
}
// Bad: Creating new client for each request
func sendEmail(to, subject, html string) error {
    client := lettr.NewClient(os.Getenv("LETTR_API_KEY")) // Don't do this
    // ...
}

Troubleshooting

If you see “The from address domain is not verified”:
  • Verify your domain in the Lettr dashboard
  • Ensure the from address uses the verified domain
  • Wait for DNS propagation (can take up to 48 hours)
  • See Domain Verification for help
If you see authentication errors:
  • Check your API key is correct and starts with lttr_
  • Verify the key is 68 characters total (prefix + 64 hex chars)
  • Ensure the key hasn’t been revoked in the dashboard
  • Confirm you’re reading from the correct environment variable
If requests timeout:
  • Increase the context timeout (default 10 seconds may be too short)
  • Check your network connectivity and firewall settings
  • Verify app.lettr.com is reachable: ping app.lettr.com
  • Consider using a custom HTTP client with longer timeouts
If you see “module not found” or import errors:
  • Run go mod tidy to sync dependencies
  • Verify the import path: github.com/lettr-com/lettr-go
  • Check your Go version is 1.21 or later: go version
  • Clear module cache: go clean -modcache
If you’re hitting rate limits:
  • Implement exponential backoff retry logic (see Best Practices)
  • Use batch sending with controlled concurrency
  • Consider upgrading your Lettr plan for higher limits
  • Spread requests over time instead of bursts

What’s Next