2.x no longer receives updates, including fixes. Upgrade to 3.0 to keep getting them.
3.0 is a new major version. The main reason is safety: in 2.x, Lettermint.email(token) returned one mutable, non-thread-safe EmailEndpoint, and the README suggested keeping it as the client. Two emails composed at the same time on it, for example in two web requests, could mix recipients, content and Idempotency-Key, and an email abandoned halfway (for example because tags() threw) leaked into the next send. 2.x also followed redirects with the sending token, could silently re-send POST /send after a connection failure, and treated a webhook tolerance of 0 as "no check". 3.0 stores nothing about a message on the client and fixes the rest.
- One thread-safe client:
Lettermint.builder().sendingToken(...).teamToken(...).build(), orLettermint.of(token). It replacesLettermint.email(),Lettermint.api(),new Lettermint(token),ApiClientandLettermintClient. - Sending is stateless:
emails().send(message, options),emails().sendBatch(messages, options)and an immutableemails().compose()builder. TheIdempotency-Keyis a per-call option. - Each part uses its own token:
emails()uses the sending token, the Team API uses the team token. The SDK never falls back to the other token. - Typed exceptions for every outcome, including empty or HTML responses, redirects, timeouts and network failures.
- Redirects are never followed, and nothing is retried.
- A configurable timeout that covers the whole request, an injectable
java.net.http.HttpClient, and cancellation by interrupting the thread. - Tokens never appear in
toString()or exception messages. - Webhook verification is an object,
new Webhook(secret), and requires both signature headers. - Typed query records and
iterate()methods that follownext_cursor. - Types are immutable records generated from the current API specification and use its names (see Type names). Enums are open.
- Java 17 or later. OkHttp is no longer a dependency.
- Java 17 or later (2.x: Java 8). Java 8 and 11 are past their mainstream support, and 3.0 relies on Java 17: records for the generated types, and the JDK's
java.net.httpclient, which since Java 16 aborts a request when its future is cancelled, so timeouts and thread interrupts really stop the request. - The only dependency is Jackson Databind 2.x. OkHttp and its Kotlin standard library are gone; if your code used OkHttp only through this SDK, you can drop it.
You can let a coding agent (Claude Code, Codex, Cursor, Copilot, …) do the upgrade. Copy this instruction into the agent from your project's root, then review its changes:
Upgrade this project from the Lettermint Java SDK 2.x (co.lettermint:lettermint) to 3.0.
1. Change the dependency to co.lettermint:lettermint:3.0.0 in pom.xml, build.gradle(.kts) or the version catalog. 3.0 needs Java 17 or newer: check the compiler release/target, toolchains, CI workflows and Dockerfiles, and report anything older.
2. Read the upgrade guide before changing code: https://github.com/lettermint/lettermint-java/blob/main/UPGRADE.md (the sources jar, co.lettermint:lettermint:3.0.0:sources, has the Javadoc of every class). Treat the guide as the source of truth and don't guess APIs.
3. Find every use of the SDK: imports from co.lettermint, Lettermint.email(, Lettermint.api(, new Lettermint(, getClient(, EmailEndpoint, ApiClient, LettermintClient, .idempotencyKey(, .attach(, .sendBatch(, Webhook.verify(, HttpRequestException, ValidationException, InvalidSignatureException, TimestampToleranceException, co.lettermint.models, and the 2.x type names from the guide's type-name table.
4. Rewrite each use following the guide's before/after examples:
- Create one Lettermint client with Lettermint.builder().sendingToken(...), adding .teamToken(...) only where the Team API is used, and share it (for example as one Spring bean). Keep the project's existing environment variable or property names.
- Replace fluent EmailEndpoint chains with lettermint.emails().send(EmailMessage.create()...) or lettermint.emails().compose()...send(). Builders are immutable: assign the result of every setter. Never keep a half-built email in a field.
- Move idempotency keys into SendOptions.idempotencyKey(...) on send()/sendBatch(). Attachments become EmailAttachment.ofBase64(filename, base64) or EmailAttachment.of(filename, bytes), with .contentType(...) and .contentId(...).
- Team API: use the same client (lettermint.domains(), ...), typed query records instead of Map<String, String> with bracket keys, record accessors (domain.id()) instead of public fields (domain.id), builders for request bodies, and the renamed methods from the guide.
- Enum constants are now objects, not strings: compare with equals() or use .value(); constant names gained underscores (SOFTBOUNCED -> SOFT_BOUNCED).
- Exceptions: switch to the 3.0 classes in co.lettermint.exceptions (ApiException and its subclasses, TimeoutException, ...). Accessors are getStatus(), getBody(), getCode().
- Webhooks: new Webhook(secret).verify(rawBody, headers). Keep passing the raw request body, keep the secret's whsec_ prefix, and make sure the X-Lettermint-Signature and X-Lettermint-Delivery headers reach the handler.
- Rename types using the guide's type-name table; co.lettermint.models.api becomes co.lettermint.types.
5. Compile and run the tests, and fix every error. Don't send real email or call the live API while testing.
6. Finish with a summary: the files you changed, anything you could not migrate with certainty, and behaviour changes I should review.
Never print, log or commit API tokens or webhook secrets.
Lettermint.email(...), Lettermint.api(...), new Lettermint(token[, baseUrl]), lettermint.email(), getClient(), ApiClient, LettermintClient and Endpoint are removed.
// 2.x
EmailEndpoint email = Lettermint.email(System.getenv("LETTERMINT_PROJECT_TOKEN"));
ApiClient api = Lettermint.api(System.getenv("LETTERMINT_API_TOKEN"), "https://api.lettermint.co/v1");
Lettermint legacy = new Lettermint(System.getenv("LETTERMINT_PROJECT_TOKEN"));
// 3.0
Lettermint lettermint = Lettermint.builder()
.sendingToken(System.getenv("LETTERMINT_PROJECT_TOKEN")) // for lettermint.emails()
.teamToken(System.getenv("LETTERMINT_TEAM_TOKEN")) // for the Team API
.baseUrl("https://api.lettermint.co/v1") // optional
.timeout(Duration.ofSeconds(10)) // new; default 30 seconds
.build();Pass one token or both. With only one, calling a part that needs the other throws LettermintConfigException (for example domains.list needs teamToken; …) before any request.
You can also pass a token string; the SDK chooses its type by the prefix:
Lettermint lettermint = Lettermint.of("lm_team_..."); // team token
Lettermint lettermint = Lettermint.of("lm_..."); // project sending token
Lettermint lettermint = Lettermint.builder().token(token).timeout(Duration.ofSeconds(10)).build(); // with optionsAny other format (SSO tokens, OAuth tokens, an empty string) throws LettermintConfigException; use sendingToken(...) or teamToken(...) for those.
New options: timeout(Duration) (2.x had a fixed 30 seconds per connect, read and write phase; 3.0 limits the whole request) and httpClient(HttpClient) for a proxy, TLS settings or an executor. The client must not follow redirects. 2.x created a new OkHttp client for every Lettermint.email(...)/Lettermint.api(...) call; create the 3.0 client once and share it.
The 2.x EmailEndpoint was the client and a mutable builder at once, and was reset after each send. In 3.0, emails are immutable values: EmailMessage, or the EmailBuilder that emails().compose() returns. Each setter returns a new object and leaves the old one unchanged. Chaining works as before; if you built an email over several statements, assign the result of each setter.
// 2.x
EmailEndpoint email = Lettermint.email(token);
SendEmailResponse response = email
.from("Acme <hello@acme.com>")
.to("jane@example.com")
.subject("Welcome")
.html("<p>Hi Jane</p>")
.idempotencyKey("welcome-jane")
.send();
String id = response.getMessageId();
// 3.0: builder
SendMailResponse response = lettermint.emails().compose()
.from("Acme <hello@acme.com>")
.to("jane@example.com")
.subject("Welcome")
.html("<p>Hi Jane</p>")
.send(SendOptions.idempotencyKey("welcome-jane"));
String id = response.messageId();
// 3.0: message value
EmailMessage message = EmailMessage.create().from("Acme <hello@acme.com>").to("jane@example.com").subject("Welcome").html("<p>Hi Jane</p>");
lettermint.emails().send(message, SendOptions.idempotencyKey("welcome-jane"));// 2.x: statements changed the shared endpoint
email.from("hello@acme.com");
email.to("jane@example.com");
if (copy) email.cc("team@acme.com");
email.subject("Hi").send();
// 3.0: keep the returned builder
EmailBuilder draft = lettermint.emails().compose().from("hello@acme.com").to("jane@example.com");
if (copy) draft = draft.cc("team@acme.com");
draft.subject("Hi").send();A base builder can now be shared safely, also between threads:
EmailBuilder welcome = lettermint.emails().compose().from("Acme <hello@acme.com>").subject("Welcome");
welcome.to("jane@example.com").html(janeHtml).send();
welcome.to("john@example.com").html(johnHtml).send(SendOptions.idempotencyKey("welcome-john"));2.x (EmailEndpoint) |
3.0 (EmailBuilder and EmailMessage) |
|---|---|
| Setters change the endpoint and return it | Return a new builder or message |
.idempotencyKey(key).send() |
.send(SendOptions.idempotencyKey(key)) |
.send() returns SendEmailResponse (getMessageId(), getStatus()) |
.send() returns SendMailResponse (messageId(), status() as MessageStatus, sandbox(), sandboxResult(), scheduledAt()) |
.attach(filename, base64), .attach(filename, base64, contentId), .attach(filename, base64, contentId, contentType) |
.attach(EmailAttachment.ofBase64(filename, base64).contentId(id).contentType(type)), or EmailAttachment.of(filename, bytes), which base64-encodes for you |
.header(name, value) |
Removed; pass every header to .headers(map) |
.metadata(Map<String, Object>), .metadata(key, value) |
.metadata(Map<String, String>) (the API stores strings) |
.tags(MessageTag...) with co.lettermint.models.MessageTag, .tags(Map<String, String>...) |
.tags(MessageTagInput...) or .tags(Collection<MessageTagInput>) with co.lettermint.types.MessageTagInput |
.settings(Map<String, Object>) |
.settings(SendMailRequestSettings) |
.sandboxResult(String) |
.sandboxResult(SandboxResult) (SandboxResult.of("…") for any value) |
.scheduledAt(String) |
.scheduledAt(String) or .scheduledAt(Instant) |
.html(null), .text(null) |
null removes the field; so do tag(null), route(null) and the other single-value setters |
Invalid tags threw IllegalArgumentException |
Throw LettermintValidationException (with getField()); the builder is unchanged |
| — | builder.build() returns the EmailMessage; message.toRequest() the wire format |
Unchanged setters: from, to, cc, bcc, replyTo (each replaces its list), subject, html, text, headers, route, tag.
// 2.x
SendMailRequest request = new SendMailRequest();
request.fromValue = "hello@acme.com";
request.to = List.of("jane@example.com");
request.subject = "Hi";
Lettermint.email(token).idempotencyKey("batch-1").sendBatch(List.of(request));
Lettermint.email(token).ping();
Lettermint.api(token).ping();
// 3.0
lettermint.emails().sendBatch(List.of(
EmailMessage.create().from("hello@acme.com").to("jane@example.com").subject("Hi"),
builder.build()), SendOptions.idempotencyKey("batch-1"));
lettermint.emails().ping(); // sending token
lettermint.ping(); // team token if configured, otherwise the sending tokenThe endpoint groups move from Lettermint.api(token).x() to lettermint.x(). Query parameters are typed records instead of Map<String, String> with bracket keys, and every list has an iterate() method that follows next_cursor.
// 2.x
ApiClient api = Lettermint.api(token);
DomainIndexResponse page = api.domains().list(Map.of("page[size]", "10", "filter[status]", "verified"));
for (DomainListData domain : page.data) { System.out.println(domain.domain); }
// 3.0
CursorPage<DomainListData> page = lettermint.domains().list(ListDomainsQuery.builder().pageSize(10).filterStatus(DomainStatus.VERIFIED).build());
for (DomainListData domain : lettermint.domains().iterate(ListDomainsQuery.builder().filterStatus(DomainStatus.VERIFIED).build())) {
System.out.println(domain.domain());
}2.x (api = Lettermint.api(token)) |
3.0 (lettermint = Lettermint.builder().teamToken(token).build()) |
|---|---|
api.ping() |
lettermint.ping() |
api.blockedFileTypes() |
lettermint.blockedFileTypes() |
api.analytics(AnalyticsRequest) |
lettermint.analytics(AnalyticsQuery) |
api.domains().list(map) |
lettermint.domains().list(ListDomainsQuery), lettermint.domains().iterate(query) |
api.domains().create(payload) |
lettermint.domains().create(payload) |
api.domains().retrieve(id) |
lettermint.domains().retrieve(id) or retrieve(id, GetDomainQuery) (include: GetDomainQueryIncludeItem.DNS_RECORDS) |
api.domains().delete(id) |
lettermint.domains().delete(id) |
api.domains().verifyDnsRecords(id) |
lettermint.domains().verifyDnsRecords(id) |
api.domains().verifyDnsRecord(id, recordId) |
lettermint.domains().verifyDnsRecord(id, recordId) |
api.domains().updateProjects(id, payload) |
lettermint.domains().updateProjects(id, payload) |
api.messages().list(map) |
lettermint.messages().list(ListMessagesQuery), lettermint.messages().iterate(query) |
api.messages().retrieve(id) |
lettermint.messages().retrieve(id) |
api.messages().events(id[, map]) |
lettermint.messages().events(id[, ListMessageEventsQuery]), lettermint.messages().iterateEvents(id[, query]) |
api.messages().source(id) / .html(id) / .text(id) |
unchanged, on lettermint.messages() |
api.messages().reschedule(id, payload) |
lettermint.messages().reschedule(id, payload); accepts either token |
api.messages().cancel(id) |
lettermint.messages().cancel(id); accepts either token |
api.messages().process(id) |
lettermint.messages().process(id[, SendOptions.idempotencyKey(key)]) |
api.projects().list(map) |
lettermint.projects().list(ListProjectsQuery), lettermint.projects().iterate(query) |
api.projects().create(payload) |
lettermint.projects().create(payload) |
api.projects().retrieve(id) |
lettermint.projects().retrieve(id[, GetProjectQuery]) |
api.projects().update(id, payload) |
lettermint.projects().update(id, payload) |
api.projects().delete(id) |
lettermint.projects().delete(id) |
api.projects().rotateToken(id) |
lettermint.projects().rotateToken(id) (deprecated by the API) |
api.projects().routes(projectId[, map]) |
lettermint.routes().list(projectId[, ListRoutesQuery]), lettermint.routes().iterate(projectId[, query]) |
api.projects().createRoute(projectId, payload) |
lettermint.routes().create(projectId, payload) |
api.projects().retrieveReportForwarding(id) |
lettermint.projects().reportForwarding().retrieve(id) |
api.projects().updateReportForwarding(id, payload) |
lettermint.projects().reportForwarding().update(id, payload) |
api.projects().deleteReportForwarding(id) |
lettermint.projects().reportForwarding().delete(id) |
api.projects().verifyReportForwarding(id, payload) |
lettermint.projects().reportForwarding().verify(id, payload) |
api.projects().resendReportForwardingCode(id) |
lettermint.projects().reportForwarding().resendCode(id) |
api.routes().retrieve(id) |
lettermint.routes().retrieve(id[, GetRouteQuery]) |
api.routes().update(id, payload) |
lettermint.routes().update(id, payload) |
api.routes().delete(id) |
lettermint.routes().delete(id) |
api.routes().verifyInboundDomain(id) |
lettermint.routes().verifyInboundDomain(id) |
api.stats().retrieve(map) |
lettermint.stats().retrieve(GetStatsQuery.builder().from(...).to(...).build()) |
api.suppressions().list(map) |
lettermint.suppressions().list(ListSuppressionsQuery), lettermint.suppressions().iterate(query) |
api.suppressions().create(payload) |
lettermint.suppressions().create(payload) |
api.suppressions().delete(id) |
lettermint.suppressions().delete(id) |
api.team().retrieve() |
lettermint.team().retrieve([GetTeamQuery]) |
api.team().update(payload) |
lettermint.team().update(payload) |
api.team().usage([map]) |
lettermint.team().usage() (the endpoint takes no parameters) |
api.team().roles() |
lettermint.team().roles() |
api.team().members([map]) |
lettermint.team().members().list([ListTeamMembersQuery]), lettermint.team().members().iterate([query]) |
api.team().member(userId) |
lettermint.team().members().retrieve(userId) |
api.team().updateMemberAssignment(userId, payload) |
lettermint.team().members().updateAssignment(userId, payload) |
api.webhooks().list(map) |
lettermint.webhooks().list(ListWebhooksQuery), lettermint.webhooks().iterate(query) |
api.webhooks().create(payload) |
lettermint.webhooks().create(payload) |
api.webhooks().retrieve(id) |
lettermint.webhooks().retrieve(id) |
api.webhooks().update(id, payload) |
lettermint.webhooks().update(id, payload) |
api.webhooks().delete(id) |
lettermint.webhooks().delete(id) |
api.webhooks().test(id) |
lettermint.webhooks().test(id) |
api.webhooks().regenerateSecret(id) |
lettermint.webhooks().regenerateSecret(id) |
api.webhooks().deliveries(id[, map]) |
lettermint.webhooks().deliveries().list(id[, ListWebhookDeliveriesQuery]), lettermint.webhooks().deliveries().iterate(id[, query]) |
api.webhooks().delivery(id, deliveryId) |
lettermint.webhooks().deliveries().retrieve(id, deliveryId) |
Every method also has an overload with a last RequestOptions argument (RequestOptions.timeout(Duration)); messages().process() takes SendOptions.
Query records have one builder method per parameter, named after the wire name in camel case. Arrays of values are joined with commas, arrays of objects are indexed, and booleans are sent as 1/0.
| 2.x | 3.0 |
|---|---|
Map.of("page[size]", "30", "page[cursor]", c) |
ListDomainsQuery.builder().pageSize(30).pageCursor(c).build() |
Map.of("filter[status]", "verified") |
.filterStatus(DomainStatus.VERIFIED) |
Map.of("sort", "-created_at,domain") |
.sort(List.of(ListDomainsQuerySortItem.CREATED_AT_DESC, ListDomainsQuerySortItem.DOMAIN)) |
Map.of("filter[enabled]", "true") |
ListWebhooksQuery.builder().filterEnabled(true) |
Map.of("filter[tags][0][name]", "a", "filter[tags][0][value]", "b") |
ListMessagesQuery.builder().filter(ListMessagesQueryFilter.builder().tags(List.of(ListMessagesQueryFilterTagsItem.builder().name("a").value("b").build())).build()) |
webhooks: Map.of("cursor", c) |
unchanged wire name: ListWebhooksQuery.builder().cursor(c) (these lists use cursor, not page[cursor]) |
IDs are still URL-encoded. An empty or null ID, . or .. now throws LettermintConfigException before the request.
2.x typed message and event lists with a meta map. The API returns a flat cursor page, which 3.0 types as CursorPage<T>: read page.nextCursor(), or use iterate().
The generated types moved from co.lettermint.models.api to co.lettermint.types and became immutable records:
// 2.x: public mutable fields
DomainData domain = api.domains().retrieve(id);
String name = domain.domain;
StoreWebhookData payload = new StoreWebhookData();
payload.name = "Hook";
payload.url = "https://acme.com/hook";
payload.events = List.of(WebhookEvent.MESSAGE_DELIVERED);
// 3.0: accessors and builders
String name = lettermint.domains().retrieve(id).domain();
StoreWebhookData payload = StoreWebhookData.builder()
.name("Hook")
.url("https://acme.com/hook")
.events(List.of(WebhookEvent.MESSAGE_DELIVERED))
.build();- Field access becomes an accessor (
domain.domain→domain.domain(); 2.xfromValue→from()). Requests are built withX.builder()…build(),toBuilder()or the record constructor. - Lists and maps in records are unmodifiable.
- Integers are
Longand numbersDouble(builders also takelonganddouble). - Enums are open objects, not strings. In 2.x,
MessageStatus.DELIVEREDwas theString"delivered"and fields wereString. In 3.0 fields are typed (MessageStatus status()), constants areMessageStatusinstances, and a value the API adds later decodes without an error:status.value()returns the raw string andstatus.isKnown()tells whether this version knows it. Compare withMessageStatus.DELIVERED.equals(status)or switch onstatus.value(); build a value withMessageStatus.of("…"). - Enum constant names gained underscores:
SOFTBOUNCED→SOFT_BOUNCED,HARDBOUNCED→HARD_BOUNCED,SPAMCOMPLAINT→SPAM_COMPLAINT,POLICYREJECTED→POLICY_REJECTED,WebhookEvent.MESSAGEDELIVERED→MESSAGE_DELIVERED, and so on. Sort values with a leading-end in_DESC(-created_at→CREATED_AT_DESC). OptionalNullablemoved toco.lettermint.types:OptionalNullable.nullValue()→ofNull(),getValue()→value(). Request builders create it for you: an optional and nullable field that you leave unset is omitted, andnullsends JSONnull.
HttpRequestException and the 2.x ValidationException are replaced. Every SDK exception still extends co.lettermint.exceptions.LettermintException (unchecked).
| Situation | 2.x | 3.0 |
|---|---|---|
| HTTP 400 and other 4xx | HttpRequestException (getStatusCode(), getResponseBody()) |
ApiException (getStatus(), getCode(), getMessage(), getDetails(), getBody()) |
| HTTP 401 | HttpRequestException |
AuthenticationException |
| HTTP 403 | HttpRequestException |
PermissionException |
| HTTP 404 | HttpRequestException |
NotFoundException |
| HTTP 409 | HttpRequestException |
ConflictException |
| HTTP 422 | ValidationException (getResponseBody() as a string) |
ValidationException (getErrors() field errors, getBody()) |
| HTTP 429 | HttpRequestException |
RateLimitException (getRetryAfter() as a Duration) |
| HTTP 5xx | HttpRequestException |
ServerException |
| Empty or invalid JSON body, HTML error page | LettermintException ("Request failed: …") or HttpRequestException |
UnexpectedResponseException (getStatus(), getBodyExcerpt()) |
| Redirect (3xx) | followed, also to another host, with the token | RedirectException (getStatus()); never followed |
| Timeout | LettermintException ("Request timed out") |
co.lettermint.exceptions.TimeoutException (getTimeout()); covers the whole request |
| Network failure | LettermintException ("Request failed: …"); OkHttp could also re-send the request |
ConnectionException; never retried |
| Invalid tags | IllegalArgumentException |
LettermintValidationException (getField()) |
| Missing or wrong token, bad option | IllegalArgumentException |
LettermintConfigException |
Property renames: getStatusCode() → getStatus(), getResponseBody() (a string) → getBody() (decoded JSON). getCode() is new and comes from {"error": {"code"}} or a string error field.
// 2.x
try {
email.send();
} catch (ValidationException e) {
log.warn(e.getResponseBody());
} catch (HttpRequestException e) {
if (e.getStatusCode() == 429) retryLater();
}
// 3.0
try {
builder.send();
} catch (ValidationException e) {
log.warn("{} {}", e.getMessage(), e.getErrors());
} catch (RateLimitException e) {
retryLater(e.getRetryAfter());
}The SDK does not retry requests. Pass an idempotency key when you retry a send. Interrupting the calling thread aborts the request and throws java.util.concurrent.CancellationException, not an SDK exception.
The static Webhook.verify(payload, signature, secret[, tolerance]) is replaced by a verifier object. verify() now takes the raw body and the request headers, requires both X-Lettermint-Signature and X-Lettermint-Delivery, and returns a WebhookPayload instead of Map<String, Object>.
// 2.x
Map<String, Object> payload = Webhook.verify(rawPayload, signatureHeader, secret);
Map<String, Object> payload = Webhook.verify(rawPayload, signatureHeader, secret, 600);
String event = (String) payload.get("event");
// 3.0
Webhook webhook = new Webhook(secret); // create once
Webhook webhook = new Webhook(secret, Duration.ofSeconds(600));
WebhookPayload payload = webhook.verify(rawBody, headers); // Map<String, String>, Map<String, List<String>>, or request::getHeader
WebhookPayload payload = webhook.verifySignature(rawBody, signatureHeader, deliveryHeader);
WebhookEvent event = payload.event();
Map<String, Object> all = payload.fields();- The body may be a
Stringor the rawbyte[]. X-Lettermint-Deliverymust be present and equal the signed timestamp. 2.x ignored it.- Every
v1signature in the header is checked, so key rotation works; 2.x used only the last one. - A tolerance of
0now accepts only the current second; 2.x skipped the timestamp check for0or less. A negative tolerance or an empty secret throwsLettermintConfigException(2.x:WebhookVerificationExceptionfor an empty secret). InvalidSignatureExceptionandTimestampToleranceExceptionare removed. Every failure is aco.lettermint.exceptions.WebhookVerificationException(it moved out ofco.lettermint.exceptions.webhook) withgetReason():SIGNATURE_HEADER_MISSING,SIGNATURE_HEADER_MALFORMED,DELIVERY_HEADER_MISSING,DELIVERY_TIMESTAMP_MISMATCH,TIMESTAMP_OUT_OF_TOLERANCE,SIGNATURE_MISMATCH,BODY_INVALIDorPAYLOAD_INVALID(reason.code()gives the shared code, such assignature_mismatch).Webhookmoved fromco.lettermint.webhookstoco.lettermint.
The types are generated from the API specification of lettermint#2582 and use its names. They moved from co.lettermint.models.api to co.lettermint.types. Some shapes also changed:
- Java has no type aliases, so list responses are
CursorPage<Item>andsendBatch()returnsList<SendMailResponse>. SendMailResponsehas every field of a pending and a scheduled send; checkstatus().PendingSendMailResponseandScheduledSendMailResponsedescribe the two cases.MessageTag(inco.lettermint.types) describes tags in responses;MessageTagInputdescribes tags you send.SuppressionStoreResponse.message()is aString(2.x:Object).
These classes keep their name: AnalyticsResponse, AttachmentDelivery, BuiltInTeamRole, DeliveryMode, DkimMode, DnsRecordPurpose, DnsRecordStatus, DnsVerificationScope, DomainData, DomainDnsRecordData, DomainListData, DomainStatus, GetReportForwardingResponse, InitialRoutes, MessageAttachmentData, MessageData, MessageEventData, MessageEventType, MessageListData, MessageRecipientData, MessageStatsData, MessageStatus, MessageType, OptionalNullable, Plan, ProcessInboundMessageResponse, ProjectAccessScope, ProjectCreatedData, ProjectData, ProjectListData, RbacConflictCode, RbacPermission, RecordType, ReportForwardingRequest, ReportForwardingResource, RescheduleMessageRequest, ResendReportForwardingCodeResponse, RouteData, RouteListData, RouteStatisticData, RouteType, SandboxResult, SendMailRequest, SendMailResponse, SpamSymbol, StatsDailyData, StatsData, StatsInboundData, StatsTotalsData, StatsTypeData, StoreDomainData, StoreProjectData, StoreRouteData, StoreSuppressionData, StoreWebhookData, SuppressedRecipientData, SuppressionAppliesTo, SuppressionReason, SuppressionScope, SuppressionSourceMessageData, SuppressionStoreResponse, SuppressionType, TeamAddonData, TeamData, TeamMemberData, TeamMemberProjectAccessData, TeamRoleData, TeamType, TeamUsageDetailData, TeamUsagePeriodData, TlsPolicy, UpdateDomainProjectsData, UpdateProjectData, UpdateReportForwardingResponse, UpdateRouteData, UpdateRouteInboundSettingsData, UpdateRouteSettingsData, UpdateTeamData, UpdateTeamMemberAssignmentData, UpdateWebhookData, VerifyReportForwardingRequest, VerifyReportForwardingResponse, WebhookBasicAuthData, WebhookData, WebhookDeliveryData, WebhookDeliveryListData, WebhookDeliveryModeFilter, WebhookDeliveryStatus, WebhookEvent, WebhookListData, WebhookScope, WebhookSecretData.
2.x (co.lettermint.models.api) |
3.0 (co.lettermint.types) |
|---|---|
AnalyticsRequest |
AnalyticsQuery |
AnalyticsRequestFiltersItem |
AnalyticsFilter |
AnalyticsRequestSort |
AnalyticsSort |
AnalyticsResponseMeta |
AnalyticsMeta |
AnalyticsResponseMetaComparison |
AnalyticsMetaComparison |
AnalyticsResponsePagination |
AnalyticsPagination |
AnalyticsResponsePayload |
AnalyticsResults |
AnalyticsResponsePayloadBreakdownItem |
AnalyticsBreakdownRow |
AnalyticsResponsePayloadSummary |
AnalyticsSummary |
AnalyticsResponsePayloadTimeSeriesItem, AnalyticsResponsePayloadBreakdownItemTrendItem |
AnalyticsTimeSeriesPoint |
| AnalyticsResponsePayload…Metrics, …PreviousMetrics (8 classes) | AnalyticsMetricValues |
| AnalyticsResponsePayload…Previous (4 classes) | AnalyticsComparisonValues |
| AnalyticsResponsePayload…RateBases, …PreviousRateBases (8 classes) | AnalyticsRateBases |
| AnalyticsResponsePayload…RateBases…Rate (56 classes, for example AnalyticsResponsePayloadSummaryRateBasesBounceRate) | AnalyticsRateBase |
BlockedFileTypesResponse |
BlockedFileTypes |
CancelScheduledMessageResponse |
ScheduledMessage |
CursorPaginator |
CursorPage<T> |
DomainDestroyResponse |
MessageResponse |
DomainIndexResponse |
CursorPage<DomainListData> |
DomainUpdateProjectsResponse |
DomainMutationResponse |
DomainVerifyDnsRecordsResponse |
DnsVerificationSuccessResponse |
DomainVerifySpecificDnsRecordResponse |
MessageResponse |
MessageEventsResponse |
CursorPage<MessageEventData> |
MessageIndexResponse |
CursorPage<MessageListData> |
ProjectDestroyResponse |
MessageResponse |
ProjectIndexResponse |
CursorPage<ProjectListData> |
ProjectRotateTokenResponse |
RotateProjectTokenResponse |
ProjectStoreResponse |
ProjectCreatedData |
ProjectUpdateResponse |
ProjectMutationResponse |
RescheduleMessageResponse |
ScheduledMessage |
RouteDestroyResponse |
MessageResponse |
RouteIndexResponse |
CursorPage<RouteListData> |
RouteStoreResponse |
RouteMutationResponse |
RouteUpdateResponse |
RouteMutationResponse |
RouteVerifyInboundDomainResponse |
InboundDomainVerificationResponse |
SuppressionDestroyResponse |
DeleteSuppressionResponse |
SuppressionIndexResponse |
CursorPage<SuppressedRecipientData> |
TeamMembersResponse |
CursorPage<TeamMemberData> |
TeamRolesResponse |
TeamRoleListResponse |
TeamUpdateResponse |
TeamMutationResponse |
UpdateReportForwardingRequest |
ReportForwardingRequest |
WebhookDeliveriesResponse |
CursorPage<WebhookDeliveryListData> |
WebhookDestroyResponse |
MessageResponse |
WebhookIndexResponse |
CursorPage<WebhookListData> |
WebhookRegenerateSecretResponse |
WebhookSecretResponse |
WebhookStoreResponse |
WebhookSecretResponse |
WebhookTestResponse |
TestWebhookResponse |
WebhookUpdateResponse |
WebhookMutationResponse |
2.x (co.lettermint.models) |
3.0 |
|---|---|
SendEmailResponse |
co.lettermint.types.SendMailResponse |
MessageTag |
co.lettermint.types.MessageTagInput |
Attachment |
co.lettermint.EmailAttachment |
lettermint#2582 removed these schemas from the API specification:
| 2.x | 3.0 |
|---|---|
AnalyticsResponseData |
Removed. Use AnalyticsResponse (data() is AnalyticsResults). |
StatsRequestData |
Removed. Use GetStatsQuery, the parameters of stats().retrieve(). |
Message list meta (MessageIndexResponse.meta, MessageEventsResponse.meta) |
Removed; 2.x had no class for it, only a Map field. Lists are flat CursorPage<T>. |
SuppressionStoreResponseMessage1 |
Removed; not exported by 2.x. SuppressionStoreResponse.message() is a String. |
| 2.x | 3.0 |
|---|---|
co.lettermint.Lettermint: new Lettermint(token[, baseUrl]), email(), getClient(), static email(token[, baseUrl]), static api(token[, baseUrl]) |
co.lettermint.Lettermint: Lettermint.builder()…build(), Lettermint.of(token), emails(), domains(), … |
co.lettermint.api.ApiClient and its nested DomainsEndpoint, MessagesEndpoint, ProjectsEndpoint, RoutesEndpoint, StatsEndpoint, SuppressionsEndpoint, TeamEndpoint, WebhooksEndpoint |
Lettermint with Domains, Messages, Projects (and ReportForwarding), Routes, Stats, Suppressions, Team (and TeamMembers), Webhooks (and WebhookDeliveries) |
co.lettermint.client.LettermintClient (get, post, put, patch, delete, getRaw, getObjectMapper) and LettermintClient.AuthMode |
Removed. Every documented endpoint has a method. |
co.lettermint.endpoints.EmailEndpoint |
Emails (lettermint.emails()) and EmailBuilder (emails().compose()), plus EmailMessage |
co.lettermint.endpoints.Endpoint |
Removed |
co.lettermint.BuildInfo |
No longer public; the version is in the jar manifest and the User-Agent |
co.lettermint.exceptions.HttpRequestException |
ApiException and its subclasses |
co.lettermint.exceptions.ValidationException (2.x) |
ValidationException (3.0, extends ApiException) |
co.lettermint.exceptions.webhook.WebhookVerificationException, InvalidSignatureException, TimestampToleranceException |
co.lettermint.exceptions.WebhookVerificationException with getReason() |
co.lettermint.webhooks.Webhook (static verify) |
co.lettermint.Webhook (new Webhook(secret).verify(…)) |
co.lettermint.models.Attachment, MessageTag, SendEmailResponse |
See the table above |
This guide covers upgrading from the latest released v1 Java SDK to v2.
- Sending email is available through
Lettermint.email(token). - The full Lettermint API is available through
Lettermint.api(token). - Sending tokens use
x-lettermint-token; full API tokens useAuthorization: Bearer. ping()returns the raw trimmedpongresponse.- API request and response model classes are generated from the OpenAPI specs.
String pong = Lettermint.email("sending-token").ping();Existing new Lettermint("token").email() sending usage still works.
ApiClient api = Lettermint.api("api-token");
DomainIndexResponse domains = api.domains().list();SendMailRequest payload = new SendMailRequest();
payload.fromValue = "sender@example.com";
payload.to = Collections.singletonList("user@example.com");
payload.subject = "Hello";
payload.text = "Hi";
List<SendMailResponse> response = Lettermint.email(token).sendBatch(Collections.singletonList(payload));