Skip to content

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

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 #36 — merge that first.

What changed

Non-breaking. Suggested bump: minor (3.1.0 → 3.2.0, released with #36).

  • New lettermint.BatchEmailMessage type. It is an EmailMessage with an optional per-email idempotency_key, and it mirrors the generated SendBatchMailItem.
  • emails.send_batch() now accepts Sequence[EmailMessage | BatchEmailMessage | EmailBuilder], so lists typed as EmailMessage still type-check. A per-email key goes out as the item's idempotency_key field.
  • The idempotency_key= argument of send_batch() is still the Idempotency-Key header of the whole request. emails.send() and builder.send() never put idempotency_key in the body.
  • To give a builder a key, spread its wire format: {**builder.build(), "idempotency_key": key}. BatchEmailMessage.attachments also accepts the base64 attachment list that build() returns, so this type-checks.
  • The SDK adds no new client-side checks. The API checks that keys are 1-255 characters and unique within a batch. Reusing a key with different content raises ConflictError with code IDEMPOTENCY_CONFLICT.
  • Docs: the README batch section and the send_batch docstring cover per-email keys. They also explain that on a partial failure, the error's details["accepted"] lists the emails already accepted (index, message_id). The existing APIError.details already holds that list, so there is no new accessor.

Usage

from lettermint import BatchEmailMessage

john: BatchEmailMessage = {**welcome.to("john@example.com").build(), "idempotency_key": "welcome-john"}
results = lettermint.emails.send_batch(
    [
        {"from": "hello@acme.com", "to": ["jane@example.com"], "subject": "Hi Jane", "text": "Hello",
         "idempotency_key": "welcome-jane"},
        john,
    ],
    idempotency_key="newsletter-2026-10",  # still the Idempotency-Key header of the request
)

Checks

  • python -m ruff check src tests scripts: all checks passed
  • python -m ruff format --check src tests scripts: all files formatted
  • python -m mypy: no issues in 33 source files
  • python scripts/unasync.py --check: up to date
  • python scripts/generate.py --check with and without the generator: ok
  • python -m pytest tests/: 250 passed on Python 3.10, 3.11, 3.12, 3.13 and 3.14
  • python -m build, twine check --strict dist/* and the wheel smoke test: passed
  • Separate mypy --strict runs: existing calls type-check unchanged (list[EmailMessage], list[EmailMessage | EmailBuilder], tuples, async builders), and so does the README snippet

Not run locally

  • Nothing. Every CI step ran locally.

Suggested release tag

v3.2.0 (together with #36)

🤖 Generated with Claude Code

@bjarn
bjarn requested a review from a team as a code owner October 18, 2026 15:19
- New BatchEmailMessage type: an EmailMessage with an optional
  idempotency_key. emails.send_batch() accepts it next to EmailMessage
  and builders; the key is sent as the email's idempotency_key field.
- The idempotency_key= argument stays the Idempotency-Key header of the
  whole request. The API validates the per-email keys.
- A builder gets a key through its wire format:
  {**builder.build(), "idempotency_key": key}.
- Document per-email keys and details["accepted"] on a partial batch
  failure in the README and the send_batch docstring.

Co-Authored-By: Claude <noreply@anthropic.com>
@bjarn
bjarn force-pushed the feat/batch-idempotency-keys branch from 6c07531 to b0dcc7d Compare October 18, 2026 16:07
@bjarn
bjarn added this pull request to stack #38 October 18, 2026 17:20
@bjarn
bjarn merged commit 92b09d0 into main Oct 18, 2026
7 checks passed
@bjarn
bjarn deleted the feat/batch-idempotency-keys branch October 18, 2026 20:37
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