Skip to content

feat!: Lettermint Java SDK 3.0 - #59

Merged
bjarn merged 6 commits into
mainfrom
feat/v3
Oct 4, 2026
Merged

bjarn merged 6 commits into
mainfrom
feat/v3

Conversation

@bjarn

@bjarn bjarn commented Oct 3, 2026 •

Copy link
Copy Markdown
Contributor

Summary

A new major version, following the Lettermint SDK design agreed for all languages. The reference is the Node.js SDK 3.0 (lettermint/lettermint-node#36). Sending holds no state on the client, types are generated from the published API spec, and every SDK passes the same conformance suite.

What changes

  • One client with sendingToken and/or teamToken. A single token string is detected by prefix: lm_team_ is a team token, lm_ plus letters and digits is a sending token, and anything else is a config error. Each part uses its own token and never falls back to the other.
  • Stateless sending: emails.send(message, options), emails.sendBatch(…) and an immutable per-message compose() builder. The idempotency key is a per-call option. Attachments are objects.
  • Typed errors with the HTTP status and API error code, plus timeout, connection, unexpected-response and redirect errors.
  • Transport: redirects are never followed, so the token never reaches another host. The timeout covers the whole request, nothing is retried automatically, and tokens never appear in logs or debug output.
  • Generated types from the published API spec (lettermint/lettermint#2582, pinned to 0b4ecbdd23) using its names. Enums are open, so new API values never break decoding.
  • Team API: same layout in every SDK, with typed queries and cursor iteration.
  • Webhooks: verify(rawBody, headers) requires both X-Lettermint-Signature and X-Lettermint-Delivery, accepts any matching v1, uses a ±300 s tolerance, and keys the HMAC on the secret as-is. Reason codes are the same in every SDK.
  • UPGRADE.md has before/after for every changed call, the full type rename table, and a ready-to-copy coding-agent instruction. The previous major no longer receives updates.

Specific to this SDK

  • Java 17 minimum (was 8). This lets the SDK use records and the JDK's java.net.http, which actually aborts on timeout. CI tests 17, 21 and 25.
  • OkHttp removed. The JDK client follows no redirects and does no silent POST retries, and Jackson is the only dependency.
  • A thread-safe immutable client is built with Lettermint.builder() or Lettermint.of(token). compose() returns an immutable builder.
  • Includes the earlier webhook clock-skew fix.
  • The release passes -PreleaseVersion from the tag and refuses SNAPSHOT versions. The broken pom.xml is removed.
  • Users on Java 8/11 or Android cannot upgrade.

Verification

  • 72 JUnit tests pass, plus Javadoc.
  • Conformance: 42/42 with --strict. 2.7.0 fails 9, including the token following a redirect to another host.

Release

Publish a GitHub release v3.0.0. It publishes to Maven Central.

Docs for all SDK majors: lettermint/lettermint#2594 (stacked on lettermint/lettermint#2590). Merge it after the releases.

🤖 Generated with Claude Code

bjarn added 6 commits October 3, 2026 13:47
Reject only when |now - timestamp| exceeds the tolerance, so small clock
skew between Lettermint and the receiver no longer fails every webhook.
src/main/java/co/lettermint/types is generated by the SDK generator
(emit java, profile next, spec 80d8ab2a2d). ./gradlew
generateTypes regenerates it with LETTERMINT_SDK_GENERATOR or
../sdk-generator; checkGeneratedTypes, part of check, verifies it or,
without the generator, its headers.

The build compiles with --release 17 on Gradle 9.6.1, and the version
comes from -PreleaseVersion instead of a 2.0.0 placeholder.
… tokens

One thread-safe client replaces Lettermint.email(), Lettermint.api(),
ApiClient and LettermintClient: Lettermint.builder().sendingToken(...)
.teamToken(...).build(), or Lettermint.of(token) with prefix detection.
emails() uses the sending token, every other part the team token, and
ping, messages.reschedule and messages.cancel accept either. A missing
token is a LettermintConfigException that names the option.

Sending holds no state: emails().send(EmailMessage, SendOptions),
sendBatch and an immutable compose() builder whose setters return new
builders. The Idempotency-Key is a per-call option. Tags are validated
before any request.

The transport uses java.net.http with redirects never followed (a 3xx
is a RedirectException), no retries, a timeout that covers the whole
request, cancellation by thread interrupt, typed exceptions for every
outcome and no tokens in toString or exception messages. OkHttp is
removed. Lists return CursorPage<T> and iterate() follows next_cursor.

Webhook verification requires X-Lettermint-Signature and
X-Lettermint-Delivery, accepts any matching v1, rejects a negative
tolerance and reports a WebhookVerificationException.Reason.

BREAKING CHANGE: every 2.x entry point, model and exception is replaced;
Java 17 or later is required. See UPGRADE.md.
The release workflow passes -PreleaseVersion instead of editing
build.gradle and the removed pom.xml, and checks the jar manifest and
BuildInfo before publishing. CI runs ./gradlew build, which includes
javadoc and the generated-header check.
UPGRADE.md has before/after examples for every changed call, the full
type rename table, the removed classes and types, and a ready-to-copy
instruction for upgrading with a coding agent.
The spec at lettermint/lettermint@0b4ecbdd23 (main, the #2582 merge) is
identical to the previous pin; only the generated file headers change.
@bjarn
bjarn requested a review from a team as a code owner October 3, 2026 22:51
@bjarn
bjarn merged commit a6e5d7e into main Oct 4, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants