Skip to content

feat: add per-email idempotency keys to batch send - #64

Merged
bjarn merged 1 commit into
feat/messages-resendfrom
feat/batch-idempotency-keys
Oct 18, 2026
Merged

bjarn merged 1 commit into
feat/messages-resendfrom
feat/batch-idempotency-keys

Conversation

@bjarn

@bjarn bjarn commented Oct 18, 2026

Copy link
Copy Markdown
Contributor

Stacked on #63 — merge that first.

What changed

Non-breaking; suggested bump: minor (released together with #63 as 3.2.0).

  • New EmailMessage.idempotencyKey(String) / idempotencyKey() and EmailBuilder.idempotencyKey(String). They set a per-email key that emails().sendBatch() sends as the item's idempotency_key. If an email with the same key and content was accepted in the last 24 hours, the API returns its original result and does not send it again.
  • sendBatch() now builds SendBatchMailItems. Its signature is unchanged (List<EmailMessage>, optional SendOptions), and messages without a key produce exactly the same JSON as before.
  • Client-side checks, in the same style as the existing tag checks (LettermintValidationException, before any request): a key must be 1-255 characters (checked in the setter), and keys must be unique within a batch (messages[i].idempotencyKey).
  • emails().send() / EmailBuilder.send() reject a message that has a per-email key, so a per-email key is never sent in a single-send body and is not confused with the Idempotency-Key header. SendOptions.idempotencyKey(...) is unchanged and remains the header for single sends and whole batches. These checks only apply to the new field, so nothing that was accepted before is rejected now.
  • The sendBatch Javadoc and README explain that if a batch fails part-way, ApiException.getDetails() has an accepted list (index, message_id). The README batch section has a snippet, and the builder table lists the new method.

Usage

lettermint.emails().sendBatch(orders.stream()
    .map(order -> receipt.to(order.email()).idempotencyKey("receipt-" + order.id()).build())
    .toList(), SendOptions.idempotencyKey("receipts-2026-10-18")); // optional whole-request key

Checks

  • ./gradlew clean build (JDK 17, with the generator; checkGeneratedTypes up to date, 205 files): BUILD SUCCESSFUL.
  • ./gradlew clean build (JDK 24, without the generator, as in CI): BUILD SUCCESSFUL, 90 tests passed. The new test covers the wire format of the per-email keys, the header option alongside them, null removing a key, length and uniqueness checks, the builder path, and rejection on single send().
  • Public API diff (javap -public) against main: additions only (idempotencyKey on EmailMessage and EmailBuilder), plus the generated MessageData constructor change from feat: add messages resend #63.

Not run locally

  • JDK 21 and 25 and Windows from the CI matrix.

Suggested release tag

v3.2.0 (together with #63)

🤖 Generated with Claude Code

@bjarn
bjarn requested a review from a team as a code owner October 18, 2026 15:19
EmailMessage.idempotencyKey(String) and EmailBuilder.idempotencyKey(String)
set a per-email key that emails().sendBatch() sends as the item's
idempotency_key. If an email with the same key and content was accepted
in the last 24 hours, the API returns its original result and does not
send it again. The SDK checks that keys are 1 to 255 characters and
unique within a batch. emails().send() rejects a message with a
per-email key; SendOptions.idempotencyKey(...) remains the
Idempotency-Key header for single sends and whole batches.

Co-Authored-By: Claude <noreply@anthropic.com>
@bjarn
bjarn force-pushed the feat/batch-idempotency-keys branch from 6190796 to cfe5b81 Compare October 18, 2026 16:08
@bjarn
bjarn added this pull request to stack #65 October 18, 2026 17:20
@bjarn
bjarn merged commit 52d03d4 into main Oct 18, 2026
8 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