Skip to content

feat: add messages resend - #39

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.

  • Regenerated src/generated/ from the latest API spec.
  • New messages.resend(messageId, body?, options?) for POST /messages/{messageId}/resend. It sends a sent message again as a new message with its own ID, to every original recipient or to the recipients given. Like messages.process, it takes an idempotencyKey option. Like reschedule and cancel, it uses the team token and falls back to the sending token when no team token is set.
  • README: resend is listed in the Team API table, with a short example and the note on the token fallback.

API changes in the regenerated types

Change Details
messages.resend endpoint POST /messages/{messageId}/resend. Optional body ResendMessageRequest { recipients?: string[] } and optional Idempotency-Key header. Answers 202 with a PendingSendMailResponse. Errors: 401, 403, 404, 409, 410 (content no longer retained), 422, 429, 500. Accepts the team or the sending token.
MessageData.resent_from_message_id Optional and nullable: resent_from_message_id?: string | null. The ID of the message this message resent; absent or null when the message is not a resend. Code that builds MessageData objects keeps compiling.
SendBatchMailItem New batch item type: every SendMailRequest field plus an optional per-email idempotency_key (1-255 characters). SendBatchMailRequest is now SendBatchMailItem[] (was SendMailRequest[]).
POST /send/batch 409 The error body can now also be an ApiError (code IDEMPOTENCY_CONFLICT).
OperationTypes Adds PendingSendMailResponse and ResendMessageRequest.

Usage

const lettermint = new Lettermint({ teamToken: process.env.LETTERMINT_TEAM_TOKEN });

// Every original recipient, no request body:
const resent = await lettermint.messages.resend('message-id');

// Some of the original recipients, safe to retry:
await lettermint.messages.resend(
  'message-id',
  { recipients: ['jane@example.com'] },
  { idempotencyKey: 'resend-order-1234' }
);

Checks

Run locally on Node 26.5.0:

  • npm run lint: passed (33 files, no fixes)
  • npm run type-check: passed
  • npm run generate:check: generated files up to date (with the generator), and the header check passed without it
  • npm run test:coverage: 316 passed, 8 suites
  • Compatibility: the v3.1.0 test suite, plus a MessageData literal without the new field and an EmailMessage[] passed to sendBatch, type-checks against this branch (tsc -p tsconfig.test.json: no errors). The only failure is the v3.1.0 coverage test, which lists every operation and so misses resend, as expected
  • npm run build: CJS, ESM and DTS builds succeeded
  • npm run test:smoke: ESM import, CJS require and type declarations ok

Not run locally

  • The Node 20, 22 and 24 CI matrix. Only Node 26 was run locally.

Suggested release tag

v3.2.0

🤖 Generated with Claude Code

bjarn and others added 2 commits October 18, 2026 17:10
- Add the `POST /messages/{messageId}/resend` operation with the
  optional `ResendMessageRequest` body (`recipients`)
- Add `resent_from_message_id` to `MessageData`
- Type batch items as `SendBatchMailItem`, which adds an optional
  per-email `idempotency_key`
- Batch send 409 responses may now be an `ApiError`

Co-Authored-By: Claude <noreply@anthropic.com>
Add `messages.resend(messageId, body?, options?)` for
`POST /messages/{messageId}/resend`. It sends a sent message again as a
new message, to every original recipient or to the `recipients` given.
Like `process`, it accepts an `idempotencyKey` option; like `reschedule`
and `cancel`, it falls back to the sending token when no team token is
configured.

Co-Authored-By: Claude <noreply@anthropic.com>
`resent_from_message_id` stays nullable and is now optional, so code that constructs `MessageData` keeps compiling.

Co-Authored-By: Claude <noreply@anthropic.com>
@bjarn
bjarn added this pull request to stack #41 October 18, 2026 17:20
@bjarn
bjarn merged commit 8f5e9ed into main Oct 18, 2026
4 checks passed
@bjarn
bjarn deleted the feat/messages-resend branch October 18, 2026 20:36
bjarn added a commit that referenced this pull request Oct 18, 2026
Stacked on #39 — merge that first.

## What changed

Non-breaking; suggested bump: **minor**.

- New exported type `BatchEmailMessage`: an `EmailMessage` with an
optional per-email `idempotency_key`. `emails.sendBatch()` now accepts
`(BatchEmailMessage | EmailBuilder)[]`. `EmailMessage[]` still works as
before.
- Each key goes in its own batch item as `idempotency_key`. It is
separate from the request-level `idempotencyKey` option, which still
sends the `Idempotency-Key` header.
- If an email with the same key and content was accepted in the last 24
hours, the API returns the original result and does not send it again. A
retried batch therefore sends only the emails that were not accepted
yet.
- Builders get a key by spreading their wire format: `{
...builder.build(), idempotency_key }`. No builder setter was added, so
`builder.send()` and `emails.send()` stay unchanged and `EmailMessage`
(the single-send type) does not get the field.
- The API validates the keys (1-255 characters, unique within a batch,
`409 Conflict` when a key is reused with different content). The SDK
adds no client-side checks.
- Docblock and README: on a mid-batch failure, `error.details.accepted`
lists the emails that were already accepted (`{ index, message_id }`).

## Usage

```ts
await lettermint.emails.sendBatch(
  [
    { ...invoice, idempotency_key: `invoice-${invoice.id}` },
    { ...welcome.to('john@example.com').build(), idempotency_key: 'welcome-john' },
  ],
  { idempotencyKey: 'batch-2026-10-18' } // optional, for the request as a whole
);
```

## Checks

Run locally on Node 26.5.0:

- `npm run lint`: passed (33 files, no fixes)
- `npm run type-check`: passed
- `npm run generate:check`: generated files up to date (with the
generator), and the header check passed without it
- `npm run test:coverage`: 319 passed, 8 suites
- `npm run build`: CJS, ESM and DTS builds succeeded
- `npm run test:smoke`: ESM import, CJS require and type declarations ok

## Not run locally

- The Node 20, 22 and 24 CI matrix. Only Node 26 was run locally.

## Suggested release tag

`v3.2.0` if released together with #39, otherwise the next minor after
it.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude <noreply@anthropic.com>
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