This guide covers advanced features of the Lettr Java SDK including attachments, templates, batch sending, error handling, Spring Boot integration, and best practices for production applications.
New to the Java SDK? Start with the Java Quickstart to learn the basics first.
Advanced Features
Multiple Recipients
Add multiple To, CC, and BCC recipients:
import java.util.List;
CreateEmailOptions email = CreateEmailOptions . builder ()
. from ( "notifications@yourdomain.com" )
. to ( List . of ( "user1@example.com" , "user2@example.com" ))
. cc ( List . of ( "cc@example.com" , "cc2@example.com" ))
. bcc ( List . of ( "bcc@example.com" ))
. subject ( "Team notification" )
. html ( "<p>This email has multiple recipients.</p>" )
. build ();
lettr . emails (). send (email);
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:
CreateEmailOptions email = CreateEmailOptions . builder ()
. from ( "notifications@yourdomain.com" )
. replyTo ( "support@yourdomain.com" )
. to ( "user@example.com" )
. subject ( "Support notification" )
. html ( "<p>Click reply to contact our support team.</p>" )
. build ();
lettr . emails (). send (email);
Attachments
Add file attachments to your emails:
import com.lettr.services.emails.model.Attachment;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Base64;
import java.util.List;
// Read file and encode to base64
byte [] fileData = Files . readAllBytes ( Paths . get ( "invoice.pdf" ));
String encodedContent = Base64 . getEncoder (). encodeToString (fileData);
Attachment attachment = Attachment . builder ()
. filename ( "invoice.pdf" )
. content (encodedContent)
. type ( "application/pdf" )
. build ();
CreateEmailOptions email = CreateEmailOptions . builder ()
. from ( "billing@yourdomain.com" )
. to ( "customer@example.com" )
. subject ( "Your invoice" )
. html ( "<p>Please find your invoice attached.</p>" )
. attachments ( List . of (attachment))
. build ();
lettr . emails (). send (email);
Attach multiple files:
byte [] pdfData = Files . readAllBytes ( Paths . get ( "document.pdf" ));
byte [] imgData = Files . readAllBytes ( Paths . get ( "logo.png" ));
List < Attachment > attachments = List . of (
Attachment . builder ()
. filename ( "document.pdf" )
. content ( Base64 . getEncoder (). encodeToString (pdfData))
. type ( "application/pdf" )
. build (),
Attachment . builder ()
. filename ( "logo.png" )
. content ( Base64 . getEncoder (). encodeToString (imgData))
. type ( "image/png" )
. build ()
);
CreateEmailOptions email = CreateEmailOptions . builder ()
. from ( "sender@yourdomain.com" )
. to ( "recipient@example.com" )
. subject ( "Files attached" )
. html ( "<p>See attached files.</p>" )
. attachments (attachments)
. build ();
lettr . emails (). send (email);
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:
import java.util.Map;
CreateEmailOptions email = CreateEmailOptions . builder ()
. from ( "notifications@yourdomain.com" )
. to ( "user@example.com" )
. templateId ( "welcome-email" )
. mergeTags ( Map . of (
"name" , "John Doe" ,
"company" , "Acme Corp" ,
"verify_url" , "https://example.com/verify/abc123" ,
"support_email" , "support@yourdomain.com"
))
. build ();
lettr . emails (). send (email);
Templates are managed in the Lettr dashboard . Use merge tags to personalize content without rebuilding HTML in your code.
Add custom email headers:
import java.util.Map;
CreateEmailOptions email = CreateEmailOptions . builder ()
. from ( "notifications@yourdomain.com" )
. to ( "user@example.com" )
. subject ( "Custom headers example" )
. html ( "<p>This email has custom headers.</p>" )
. headers ( Map . of (
"X-Campaign-ID" , "summer-2024" ,
"X-Priority" , "high" ,
"X-Mailer" , "Acme Mailer v1.0"
))
. build ();
lettr . emails (). send (email);
Tracking
Enable open and click tracking:
import com.lettr.services.emails.model.EmailOptions;
CreateEmailOptions email = CreateEmailOptions . builder ()
. from ( "marketing@yourdomain.com" )
. to ( "user@example.com" )
. subject ( "Newsletter" )
. html ( "<p>Check out <a href='https://example.com'>our website</a>!</p>" )
. options ( EmailOptions . builder ()
. clickTracking ( true )
. openTracking ( true )
. build ())
. build ();
lettr . emails (). send (email);
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:
import java.util.Map;
CreateEmailOptions email = CreateEmailOptions . builder ()
. from ( "notifications@yourdomain.com" )
. to ( "user@example.com" )
. subject ( "Order confirmation" )
. html ( "<p>Your order has been confirmed.</p>" )
. metadata ( Map . of (
"user_id" , "12345" ,
"order_id" , "ORD-98765" ,
"campaign" , "abandoned-cart" ,
"environment" , "production"
))
. build ();
lettr . emails (). send (email);
Metadata is returned in webhook events and can be used to correlate emails with your application data.
Error Handling
The SDK provides structured exception types for different error scenarios:
import com.lettr.core.exception.LettrException;
import com.lettr.core.exception.LettrValidationException;
import com.lettr.core.exception.LettrAuthException;
import com.lettr.core.exception.LettrRateLimitException;
CreateEmailOptions email = CreateEmailOptions . builder ()
. from ( "invalid@unverified-domain.com" )
. to ( "user@example.com" )
. subject ( "Test" )
. html ( "<p>Test</p>" )
. build ();
try {
CreateEmailResponse response = lettr . emails (). send (email);
System . out . println ( "Email sent: " + response . getRequestId ());
} catch ( LettrValidationException e ) {
// 422 validation errors
System . err . println ( "Validation failed: " + e . getMessage ());
e . getErrors (). forEach ((field, messages) ->
System . err . println ( " " + field + ": " + messages));
} catch ( LettrAuthException e ) {
// 401 authentication errors
System . err . println ( "Authentication failed: " + e . getMessage ());
} catch ( LettrRateLimitException e ) {
// 429 rate limit errors
System . err . println ( "Rate limit exceeded: " + e . getMessage ());
} catch ( LettrException e ) {
// Other API errors (500, 503, etc.)
System . err . println ( "API error: " + e . getMessage ());
} catch ( Exception e ) {
// Network or other errors
System . err . println ( "Request failed: " + e . getMessage ());
}
Common Error Scenarios
Unverified domain:
Exception: LettrValidationException
Message: “The from address domain is not verified”
Solution: Verify your domain in the dashboard
Invalid API key:
Exception: LettrAuthException
Message: “Invalid API key”
Solution: Check your API key is correct and active
Rate limit exceeded:
Exception: LettrRateLimitException
Message: “Too many requests”
Solution: Implement exponential backoff retry logic
Batch Sending
Send multiple emails concurrently using CompletableFuture:
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors;
public class BatchSender {
public static void main ( String [] args ) {
Lettr lettr = new Lettr ( System . getenv ( "LETTR_API_KEY" ));
List < String > recipients = List . of (
"user1@example.com" ,
"user2@example.com" ,
"user3@example.com"
);
List < CompletableFuture < Void >> futures = recipients . stream ()
. map (recipient -> CompletableFuture . runAsync (() -> {
CreateEmailOptions email = CreateEmailOptions . builder ()
. from ( "notifications@yourdomain.com" )
. to (recipient)
. subject ( "Batch notification" )
. html ( "<p>This is a batch email.</p>" )
. build ();
try {
CreateEmailResponse response = lettr . emails (). send (email);
System . out . println ( "Sent to " + recipient + ": " + response . getRequestId ());
} catch ( Exception e ) {
System . err . println ( "Failed to send to " + recipient + ": " + e . getMessage ());
}
}))
. collect ( Collectors . toList ());
// Wait for all to complete
CompletableFuture . allOf ( futures . toArray ( new CompletableFuture [ 0 ])). join ();
System . out . println ( "All emails sent" );
}
}
With Concurrency Limiting
Use an ExecutorService to limit concurrent requests:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
public class BatchSender {
public static void sendBatch ( Lettr lettr , List < String > recipients , int maxConcurrent ) {
ExecutorService executor = Executors . newFixedThreadPool (maxConcurrent);
for ( String recipient : recipients) {
executor . submit (() -> {
CreateEmailOptions email = CreateEmailOptions . builder ()
. from ( "notifications@yourdomain.com" )
. to (recipient)
. subject ( "Batch email" )
. html ( "<p>Hello!</p>" )
. build ();
try {
CreateEmailResponse response = lettr . emails (). send (email);
System . out . println ( "Sent to " + recipient);
} catch ( Exception e ) {
System . err . println ( "Failed: " + e . getMessage ());
}
});
}
executor . shutdown ();
try {
executor . awaitTermination ( 5 , TimeUnit . MINUTES );
} catch ( InterruptedException e ) {
Thread . currentThread (). interrupt ();
}
}
public static void main ( String [] args ) {
Lettr lettr = new Lettr ( System . getenv ( "LETTR_API_KEY" ));
List < String > recipients = List . of ( "user1@example.com" , "user2@example.com" );
sendBatch (lettr, recipients, 10 ); // Max 10 concurrent requests
}
}
For large batches in production, consider using a message queue or job processing framework like Spring Batch or Quartz to manage concurrency and handle failures gracefully.
Spring Boot Integration
Configuration
Configure the Lettr client as a Spring bean:
import com.lettr.Lettr;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@ Configuration
public class LettrConfig {
@ Value ( "${lettr.api.key}" )
private String apiKey ;
@ Bean
public Lettr lettrClient () {
return new Lettr (apiKey);
}
}
# application.properties
lettr.api.key =${LETTR_API_KEY}
Email Service
Create an email service that uses dependency injection:
import com.lettr.Lettr;
import com.lettr.services.emails.model.CreateEmailOptions;
import com.lettr.services.emails.model.CreateEmailResponse;
import org.springframework.stereotype.Service;
@ Service
public class EmailService {
private final Lettr lettr ;
public EmailService ( Lettr lettr ) {
this . lettr = lettr;
}
public String sendWelcomeEmail ( String recipient , String name ) {
CreateEmailOptions email = CreateEmailOptions . builder ()
. from ( "notifications@yourdomain.com" )
. to (recipient)
. subject ( "Welcome to Acme" )
. html ( "<h1>Welcome, " + name + "!</h1>" )
. build ();
try {
CreateEmailResponse response = lettr . emails (). send (email);
return response . getRequestId ();
} catch ( Exception e ) {
throw new RuntimeException ( "Failed to send email" , e);
}
}
}
REST Controller
Use the email service in a REST controller:
import org.springframework.web.bind.annotation. * ;
@ RestController
@ RequestMapping ( "/api/emails" )
public class EmailController {
private final EmailService emailService ;
public EmailController ( EmailService emailService ) {
this . emailService = emailService;
}
@ PostMapping ( "/welcome" )
public ResponseEntity < String > sendWelcome (@ RequestParam String email , @ RequestParam String name ) {
try {
String requestId = emailService . sendWelcomeEmail (email, name);
return ResponseEntity . ok ( "Email sent: " + requestId);
} catch ( Exception e ) {
return ResponseEntity . internalServerError (). body ( "Failed: " + e . getMessage ());
}
}
}
Best Practices
Use Environment Variables
Never hardcode API keys. Use environment variables or a configuration management system:
String apiKey = System . getenv ( "LETTR_API_KEY" );
if (apiKey == null || apiKey . isEmpty ()) {
throw new IllegalStateException ( "LETTR_API_KEY is required" );
}
Lettr lettr = new Lettr (apiKey);
Validate Before Sending
Validate email addresses before making API calls:
import java.util.regex.Pattern;
public class EmailValidator {
private static final Pattern EMAIL_PATTERN =
Pattern . compile ( "^[A-Za-z0-9+_.-]+@[A-Za-z0-9.-]+ \\ .[A-Z|a-z]{2,}$" );
public static boolean isValidEmail ( String email ) {
return EMAIL_PATTERN . matcher (email). matches ();
}
}
if ( ! EmailValidator . isValidEmail (recipient)) {
System . err . println ( "Invalid email address: " + recipient);
return ;
}
Log Request IDs
Always log the requestId from successful sends for tracking and debugging:
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
private static final Logger logger = LoggerFactory . getLogger ( Main . class );
CreateEmailResponse response = lettr . emails (). send (email);
logger . info ( "Email sent successfully - Request ID: {}, Accepted: {}" ,
response . getRequestId (), response . getAccepted ());
Handle Errors Gracefully
Implement retry logic for transient errors:
public class EmailSender {
private static final int MAX_RETRIES = 3 ;
public String sendWithRetry ( Lettr lettr , CreateEmailOptions email ) throws Exception {
for ( int attempt = 0 ; attempt <= MAX_RETRIES; attempt ++ ) {
try {
CreateEmailResponse response = lettr . emails (). send (email);
System . out . println ( "Email sent: " + response . getRequestId ());
return response . getRequestId ();
} catch ( LettrValidationException e ) {
// Don't retry validation errors
throw e;
} catch ( Exception e ) {
if (attempt < MAX_RETRIES) {
long backoff = (attempt + 1 ) * 1000L ;
System . err . println ( "Attempt " + (attempt + 1 ) + " failed, retrying in " + backoff + "ms: " + e . getMessage ());
Thread . sleep (backoff);
} else {
throw e;
}
}
}
throw new RuntimeException ( "Max retries exceeded" );
}
}
Reuse the Client
Create a single client instance and reuse it across requests:
// Good: Create once, reuse
public class EmailService {
private final Lettr lettr ;
public EmailService ( String apiKey ) {
this . lettr = new Lettr (apiKey);
}
public void sendEmail ( String to , String subject , String html ) {
CreateEmailOptions email = CreateEmailOptions . builder ()
. from ( "notifications@yourdomain.com" )
. to (to)
. subject (subject)
. html (html)
. build ();
lettr . emails (). send (email);
}
}
// Bad: Creating new client for each request
public void sendEmail ( String to, String subject, String html) {
Lettr lettr = new Lettr ( System . 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 or config
If requests timeout:
Increase the HTTP client timeout (default may be too short)
Check your network connectivity and firewall settings
Verify app.lettr.com is reachable
Use a custom HttpClient with longer timeout settings
Dependency or build errors
If you see Maven/Gradle errors:
Run mvn clean install or gradle clean build to refresh dependencies
Check your Java version is 11 or later: java -version
Verify the artifact ID is correct: com.lettr:lettr-java
Clear your local Maven repository: ~/.m2/repository
Rate limit exceeded (429 error)
If you’re hitting rate limits:
Implement exponential backoff retry logic (see Best Practices)
Use ExecutorService for controlled concurrent sending
Consider upgrading your Lettr plan for higher limits
Spread requests over time instead of bursts
Serialization or JSON errors
If you encounter JSON parsing errors:
Ensure all required builder fields are provided
Check that email addresses are properly formatted
Verify attachment content is properly base64-encoded
Use proper character encoding (UTF-8) for email content
What’s Next