SDKs

Java

Use the official Lettermint Java SDK to send email from a JVM application. Install it with Maven or Gradle. The SDK covers the Sending API and the Team API, uses the JDK's built-in HTTP client, and depends only on Jackson. See Integrations for the SDK capability overview.

This guide covers SDK 3.0. If your project uses 2.x, follow the Java SDK 3.0 upgrade guide on GitHub.

Requirements

Before you start you need:

If your domain is not verified yet, follow the domain setup guide first so your mail reaches the inbox instead of the spam folder.

1. Installation

Add the SDK to your project using Maven or Gradle:

<dependency> <groupId>co.lettermint</groupId> <artifactId>lettermint</artifactId> <version>3.0.0</version> </dependency>

2. Send your first email

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

JavaCode
import co.lettermint.Lettermint; Lettermint lettermint = Lettermint.builder() .sendingToken(System.getenv("LETTERMINT_PROJECT_TOKEN")) .build();

The client is immutable and thread-safe and stores nothing about a message, so create it once and share it across your app, for example as a Spring bean. You can also pass the token as a string, as in Lettermint.of(token).

Send your first email:

JavaCode
import co.lettermint.EmailMessage; import co.lettermint.types.SendMailResponse; SendMailResponse response = lettermint.emails().send(EmailMessage.create() .from("John Doe <john@yourdomain.com>") .to("recipient@example.com") .subject("Hello from Lettermint!") .text("Hello! This is a test email.")); System.out.println("Email sent with ID: " + response.messageId());

EmailMessage is an immutable value: every setter returns a new message and leaves the original unchanged.

The Team API uses a separate Team API token. Pass it with teamToken(...), alone or next to sendingToken(...). 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 lettermint.emails().compose(), a fluent builder with the same setters as EmailMessage plus send(). Every setter returns a new builder and leaves the original unchanged, so one builder is safe to share and reuse, also across threads. Keep the value that a setter returns when you build an email over several statements:

JavaCode
import co.lettermint.EmailBuilder; EmailBuilder draft = lettermint.emails().compose() .from("John Doe <john@yourdomain.com>") .to("recipient@example.com") .subject("Your invoice"); if (copyAccountant) { draft = draft.cc("accountant@yourdomain.com"); } draft.html("<p>Your invoice is ready.</p>").send();

Each builder example is equivalent to an EmailMessage passed to lettermint.emails().send(). Use whichever reads better.

Basic email

Send a simple text or HTML email:

JavaCode
lettermint.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();

Multiple recipients

Send to multiple recipients using CC and BCC:

JavaCode
lettermint.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();

Custom headers and reply-to

Add custom headers and set reply-to addresses:

JavaCode
import java.util.Map; lettermint.emails().compose() .from("support@yourdomain.com") .to("customer@example.com") .replyTo("help@yourdomain.com") .subject("Support Ticket #12345") .headers(Map.of("X-Priority", "1", "X-Ticket-ID", "12345")) .html("<p>Your support ticket has been updated.</p>") .send();

Metadata

Add metadata for tracking and webhook payloads. Metadata values are strings:

JavaCode
import java.util.Map; lettermint.emails().compose() .from("notifications@yourdomain.com") .to("user@example.com") .subject("Order Confirmation") .metadata(Map.of( "order_id", "12345", "customer_id", "cust_789", "campaign", "order_confirmation")) .html("<p>Your order has been confirmed.</p>") .send();

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:

JavaCode
import co.lettermint.types.MessageTagInput; lettermint.emails().compose() .from("alerts@yourdomain.com") .to("admin@example.com") .subject("System Alert") .tags( new MessageTagInput("category", "system-alerts"), new MessageTagInput("environment", "production")) .html("<p>Critical system alert detected.</p>") .send();

tags(...) accepts up to 20 name and value pairs. The SDK checks names and values before it sends the request and throws LettermintValidationException if one is invalid. tag(...) still sets the existing singular tag, which has no deprecation date. See Tags documentation.

Route selection

Direct emails to specific routes within your project:

JavaCode
lettermint.emails().compose() .from("notifications@yourdomain.com") .to("user@example.com") .subject("Welcome!") .route("transactional") .html("<p>Welcome to our platform.</p>") .send();

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:

JavaCode
import co.lettermint.SendOptions; lettermint.emails().compose() .from("notifications@yourdomain.com") .to("user@example.com") .subject("Order Confirmation") .html("<p>Your order has been confirmed.</p>") .send(SendOptions.idempotencyKey("order-confirmation-12345"));

emails().send(message, options) and emails().sendBatch(messages, options) take the same SendOptions. The key applies only to that call, and the SDK never retries on its own.

Derive the key from something stable in your domain, such as an order or invoice ID, so the same business event always maps to the same key. See the idempotency documentation for the full behaviour.

File attachments

Attach files to your emails. EmailAttachment.of takes raw bytes and encodes them to base64 for you. EmailAttachment.ofBase64 takes content that is already encoded:

JavaCode
import co.lettermint.EmailAttachment; import java.nio.file.Files; import java.nio.file.Path; byte[] fileContent = Files.readAllBytes(Path.of("/path/to/document.pdf")); lettermint.emails().compose() .from("invoices@yourdomain.com") .to("customer@example.com") .subject("Your Invoice") .html("<p>Please find your invoice attached.</p>") .attach(EmailAttachment.of("invoice.pdf", fileContent).contentType("application/pdf")) .send();

To show an attachment inside the HTML body, set contentId(...) and reference it as cid: in the markup. See Inline images.

Batch sending

Send up to 500 emails in one request. sendBatch returns one response per message:

JavaCode
import co.lettermint.EmailBuilder; import co.lettermint.EmailMessage; import co.lettermint.SendOptions; import co.lettermint.types.SendMailResponse; import java.util.List; EmailBuilder base = lettermint.emails().compose() .from("John Doe <john@yourdomain.com>") .subject("Your weekly summary"); List<SendMailResponse> results = lettermint.emails().sendBatch(List.of( EmailMessage.create() .from("John Doe <john@yourdomain.com>") .to("user1@example.com") .subject("Hi") .text("Hello"), base.to("user2@example.com").text("Your summary is ready.").build()), SendOptions.idempotencyKey("weekly-summary-2026-10-05"));

4. Send an order confirmation

A real transactional send usually pulls several of these features together. This example sends a receipt with an HTML and plain-text body, tags it for analytics, attaches the order and customer IDs as metadata, and uses an idempotency key so a retry never emails the customer twice:

JavaOrderConfirmation.java
import co.lettermint.Lettermint; import co.lettermint.SendOptions; import co.lettermint.types.MessageTagInput; import co.lettermint.types.SendMailResponse; import java.util.Map; public class OrderConfirmation { private final Lettermint lettermint = Lettermint.builder() .sendingToken(System.getenv("LETTERMINT_PROJECT_TOKEN")) .build(); public SendMailResponse send(Order order) { return lettermint.emails().compose() .from("Acme Store <orders@yourdomain.com>") .to(order.getCustomerEmail()) .replyTo("support@yourdomain.com") .subject("Order " + order.getNumber() + " confirmed") .html("<h1>Thanks for your order</h1><p>Order " + order.getNumber() + " totalling " + order.getTotal() + " is confirmed.</p>") .text("Thanks for your order. Order " + order.getNumber() + " totalling " + order.getTotal() + " is confirmed.") .tags(new MessageTagInput("type", "order-confirmation")) .metadata(Map.of( "order_id", order.getId(), "customer_id", order.getCustomerId())) .route("transactional") .send(SendOptions.idempotencyKey("order-confirmation-" + order.getId())); } }

The metadata you attach here travels with every webhook event for the message, so a later delivery or bounce maps straight back to the order in your database.

5. Response

send() returns a SendMailResponse, a record from co.lettermint.types:

JavaCode
SendMailResponse response = lettermint.emails().compose() .from("John Doe <john@yourdomain.com>") .to("recipient@example.com") .subject("Test") .text("Hello!") .send(); System.out.println(response.messageId()); // Email ID System.out.println(response.status()); // Current status

status() is a MessageStatus. Compare it with MessageStatus.SCHEDULED.equals(response.status()) or read the raw string with status().value(). Enums in the SDK are open: a value that the API adds later decodes without an error and keeps its raw string.

6. Handle errors

Every exception the SDK throws is unchecked and extends LettermintException. API errors extend ApiException, which has getStatus(), getCode(), and getMessage(). Catch the subclasses you want to handle:

JavaCode
import co.lettermint.SendOptions; import co.lettermint.exceptions.ApiException; import co.lettermint.exceptions.RateLimitException; import co.lettermint.exceptions.TimeoutException; import co.lettermint.exceptions.ValidationException; try { lettermint.emails().send(message, SendOptions.idempotencyKey(key)); } catch (ValidationException error) { System.err.println(error.getMessage() + " " + error.getErrors()); } catch (RateLimitException error) { // Wait error.getRetryAfter(), then retry with the same idempotency key. } catch (TimeoutException error) { // The outcome is unknown. Retry with the same idempotency key. } catch (ApiException error) { System.err.println(error.getStatus() + " " + error.getCode() + " " + error.getMessage()); }

The SDK never retries on its own. Reuse the same idempotency key when you retry so the customer gets one email. Other exceptions include AuthenticationException, PermissionException, NotFoundException, ConflictException, ServerException, ConnectionException, LettermintConfigException for a missing token, and LettermintValidationException when the SDK rejects a request before it sends it. All of them are in co.lettermint.exceptions. Note that co.lettermint.exceptions.TimeoutException is not java.util.concurrent.TimeoutException.

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. The SDK verifies each delivery with new Webhook(secret).verify(rawBody, headers). See Webhook signatures.
  • Test addresses let you trigger a hard or soft bounce on demand while you build and verify your webhook handler.

Tracking is configured per route, so turn it on for the route your app sends through, then read the results from your dashboard or your webhook endpoint.

FAQ

Which Java versions are supported?

Java 17 or newer (the SDK is tested on 17, 21, and 25). Add the SDK with a single Maven or Gradle dependency, and it works in any JVM app, including Spring Boot services and batch jobs. It depends only on Jackson.

How do I stop the same email being sent twice?

Pass SendOptions.idempotencyKey(...) to send() or sendBatch(), derived from a stable ID such as an order number. A repeated request with the same key returns the original result instead of sending again. See the idempotency documentation.

How do I handle send errors?

Wrap send() in a try/catch. The SDK throws ValidationException for 422 errors such as invalid parameters or an exceeded limit, RateLimitException for 429, other ApiException subclasses for other HTTP errors, and TimeoutException or ConnectionException when no response arrives.

Is Lettermint email GDPR compliant and EU-hosted?

Yes. Lettermint runs exclusively on European infrastructure and processes email in line with GDPR, so transactional mail from your Java app is handled inside the EU.

How do I track opens, clicks, and bounces?

Enable tracking on your route and subscribe to webhooks to receive delivery, open, and bounce events. Use the test addresses to simulate bounces while you build your 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.