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.
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.
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
Authentication failed (401 error)
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
Context deadline exceeded
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
Rate limit exceeded (429 error)
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