Skip to content

feat: add messages resend - #36

Merged
bjarn merged 3 commits into
mainfrom
feat/messages-resend
Oct 18, 2026
Merged

bjarn merged 3 commits into
mainfrom
feat/messages-resend

Conversation

@bjarn

@bjarn bjarn commented Oct 18, 2026 •

Copy link
Copy Markdown
Contributor

What changed

Non-breaking. Suggested bump: minor (3.1.0 → 3.2.0).

  • Regenerated lettermint.types and the operation table from the latest API spec.
  • New messages.resend(message_id, body=None, *, idempotency_key=None, timeout=None) on Lettermint and AsyncLettermint. It returns a PendingSendMailResponse for the new message.
    • Without a body, the message goes to every original recipient. {"recipients": [...]} sends it to only some of them. If you pass no body, the request has no body.
    • idempotency_key= sets the Idempotency-Key header, like messages.process() and emails.send().
    • Like messages.reschedule() and messages.cancel(), it uses the team token when one is set. Otherwise it uses the sending token.
    • Errors map to the existing classes. 410 (the content is no longer retained) raises a plain APIError. RESEND_LIMIT_REACHED (429) raises RateLimitError.
  • README: the resend method is in the messages table, the token note and a short example.

API changes in the regenerated types

Change Where
New operation POST /messages/{messageId}/resend → messages.resend. Body ResendMessageRequest { recipients?: list[str] } (optional), optional Idempotency-Key header, 202 → PendingSendMailResponse. Errors: 401, 403, 404, 409, 410, 422, 429, 500 operations.py, types.ResendMessageRequest
MessageData.resent_from_message_id: optional and nullable (NotRequired[str | None]). It holds the ID of the message this one resent and is absent or None when the message is not a resend. Code that builds MessageData dicts keeps type-checking types.MessageData
POST /send/batch items are now SendBatchMailItem, which has every SendMailRequest field plus an optional per-email idempotency_key (1-255 characters). SendBatchMailRequest = list[SendBatchMailItem] types.SendBatchMailItem
POST /send/batch 409 can now also be an ApiErrorBody (IDEMPOTENCY_CONFLICT) operations.py
Description updates (25 MB request limit / 413 on the send endpoints, 409 semantics) docs only

SendMailRequest and EmailMessage are unchanged.

Usage

from lettermint import Lettermint

lettermint = Lettermint(team_token=os.environ["LETTERMINT_TEAM_TOKEN"])

resent = lettermint.messages.resend("message-id")  # to every original recipient
resent = lettermint.messages.resend(
    "message-id",
    {"recipients": ["jane@example.com"]},
    idempotency_key="resend-message-id-jane",
)
print(resent["message_id"], resent["status"])  # the new message, "pending"

new_message = lettermint.messages.retrieve(resent["message_id"])
print(new_message.get("resent_from_message_id"))  # "message-id"

Checks

  • python -m ruff check src tests scripts: all checks passed
  • python -m ruff format --check src tests scripts: 26 files already formatted
  • python -m mypy: no issues in 33 source files
  • python scripts/unasync.py --check: src/lettermint/_sync is up to date
  • python scripts/generate.py --check with the generator: no differences, headers ok. Without the generator (as in CI): headers ok
  • python -m pytest tests/: 246 passed on Python 3.10, 3.11, 3.12, 3.13 and 3.14
  • python -m build and twine check --strict dist/*: passed. The wheel smoke test (UPGRADE.md and py.typed are in the wheel, and the client imports): passed
  • Compatibility against main: every generated TypedDict has the same required keys as before (resent_from_message_id is a new optional key), and no exports were removed. A mypy --strict file with the existing send_batch call shapes passes

Not run locally

  • Nothing. Every CI step ran locally.

Suggested release tag

v3.2.0 (together with the batch per-email idempotency keys PR)

🤖 Generated with Claude Code

bjarn and others added 2 commits October 18, 2026 17:08
- New operation: POST /messages/{messageId}/resend (team token) with an
  optional ResendMessageRequest body (recipients) and an optional
  Idempotency-Key header. Returns 202 with a PendingSendMailResponse.
- MessageData gains resent_from_message_id.
- POST /send/batch items are now SendBatchMailItem: every SendMailRequest
  field plus an optional per-email idempotency_key.
- POST /send/batch documents an ApiErrorBody for 409.
- Description updates.

Co-Authored-By: Claude <noreply@anthropic.com>
- messages.resend(message_id, body=None, *, idempotency_key=None,
  timeout=None) sends the stored content of a sent message again as a
  new message and returns the pending send response. Without a body it
  resends to every original recipient; {"recipients": [...]} chooses
  some of them. No body is sent when none is given.
- Like reschedule and cancel, it uses the team token when it is set and
  otherwise the sending token.
- Document it in the README and test the request, both tokens, the
  Idempotency-Key header and the 410 and 429 errors.

Co-Authored-By: Claude <noreply@anthropic.com>
The field stays nullable and is now optional, so code that constructs MessageData keeps type-checking.

Co-Authored-By: Claude <noreply@anthropic.com>
@bjarn
bjarn added this pull request to stack #38 October 18, 2026 17:20
@bjarn
bjarn merged commit 1b0ac44 into main Oct 18, 2026
7 checks passed
@bjarn
bjarn deleted the feat/messages-resend 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