Send email

Idempotency

Use an idempotency key when your application may retry a send request. Lettermint returns the first result instead of sending the same email twice.

Overview

Idempotency ensures that retrying a request (due to network failures, timeouts, or application errors) won't result in duplicate emails. This is critical for transactional emails where sending the same message twice can confuse recipients or cause unintended actions.

Common scenarios where idempotency helps:

  • Network timeout occurs but the email was actually sent
  • Your application crashes after sending but before recording success
  • Load balancer retries a request that succeeded on the first attempt

How it works

When you include an Idempotency-Key header, Lettermint:

  1. Hashes the request body to create a fingerprint of your email content
  2. Stores the key + hash for 24 hours, scoped to your project
  3. On subsequent requests with the same key:
    • If the body hash matches → returns the cached response (no duplicate sent)
    • If the body hash differs → returns a 409 Conflict error

Idempotency keys are scoped per project. The same key can be used across different projects without conflict. Within a single project, each key must be unique per email.

Usage

Code
Idempotency-Key: <your-unique-key>

The header also works when you resend an email.

Key requirements

RequirementValue
Length1–255 characters
FormatAny string (UUID v4 recommended)
Validity24 hours from first use
ScopePer project

Request scenarios

ScenarioResult
First request with new keyEmail sent, response cached
Same key + same body (retry)Cached response returned, no duplicate
Same key + different body409 Conflict error
Same key + request in progress409 Conflict error
Same key after an error before any email was queued, such as a validation errorProcessed as a new request
Same key after a 5xx error whose outcome is unknown409 Conflict until the key expires
Key used 24+ hours agoTreated as new key

Examples

Send with idempotency key

import { Lettermint } from "lettermint"; const lettermint = new Lettermint({ sendingToken: process.env.LETTERMINT_PROJECT_TOKEN!, }); const response = await lettermint.emails.send( { from: "John Doe <john@yourdomain.com>", to: ["recipient@example.com"], subject: "Order Confirmation #12345", html: "<p>Your order has been confirmed.</p>", }, { idempotencyKey: "order-12345-confirmation" }, ); console.log(`Email sent: ${response.message_id}`);

Response (202 accepted)

JSONCode
{ "message_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479", "status": "pending" }

Safe retry (same key + same body)

If you retry the exact same request with the same idempotency key and body within 24 hours, you receive the cached 202 Accepted response with the same message_id. No duplicate email is sent.

JSONCode
{ "message_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479", "status": "pending" }

Conflict (same key + different body)

If you use an existing key with a different request body:

HTTP 409 Conflict

JSONCode
{ "message": "This idempotency key has already been used on a request with a different payload. Retrying this request requires changing the idempotency key or payload." }

Concurrent requests

If another request with the same key is still being processed:

HTTP 409 Conflict

JSONCode
{ "message": "Another request with the same idempotency key is currently being processed. Please retry this request later." }

When you receive a concurrent request error, wait briefly and retry. The original request will complete shortly.

Batch sending

A batch sent to POST /v1/send/batch can fail halfway: emails are accepted in order, so when the third email fails, the first two may already be sent. To retry part of a batch safely, give each email an idempotency_key of its own.

TerminalcURL
curl -X POST https://api.lettermint.co/v1/send/batch \ -H "x-lettermint-token: $LETTERMINT_PROJECT_TOKEN" \ -H "Content-Type: application/json" \ -d '[ { "from": "John Doe <john@yourdomain.com>", "to": ["anna@example.com"], "subject": "Your order has shipped", "html": "<p>Order #12345 is on its way.</p>", "idempotency_key": "order-12345-shipped" }, { "from": "John Doe <john@yourdomain.com>", "to": ["ben@example.com"], "subject": "Your order has shipped", "html": "<p>Order #12346 is on its way.</p>", "idempotency_key": "order-12346-shipped" } ]'
  • An email whose key was accepted in the last 24 hours with the same content returns its original result and is not sent again, in any later batch and at any position.
  • Keys are scoped per project, must be unique within a batch, and are 1 to 255 characters.
  • When an email fails, the error lists the emails accepted before it in details.accepted, with their index and message_id. Fix the failed email and send the batch again with the same keys.
  • When a key was used before with different content, or its email is still being processed or may already have been sent, the whole batch is rejected with 409 IDEMPOTENCY_CONFLICT and nothing is sent. details.conflicts lists those emails. For an email with outcome_unknown: true, check whether it was sent before you send it again with a new key.

SMTP

The Idempotency-Key header works the same way when sending via SMTP relay.

import nodemailer from "nodemailer"; const transporter = nodemailer.createTransport({ host: "smtp.lettermint.co", port: 587, secure: false, auth: { user: "lettermint", pass: process.env.LETTERMINT_PROJECT_TOKEN, }, }); await transporter.sendMail({ from: "sender@yourdomain.com", to: "recipient@example.com", subject: "Order Confirmation #12345", html: "<p>Your order has been confirmed.</p>", headers: { "Idempotency-Key": "order-12345-confirmation" } });

Error reference

Error CodeHTTP StatusMessage
invalid_idempotency_key422The idempotency key must be between 1-255 characters.
invalid_idempotent_request409This idempotency key has already been used on a request with a different payload.
concurrent_idempotent_requests409Another request with the same idempotency key is currently being processed, or an earlier one failed after its email may have been sent.
IDEMPOTENCY_CONFLICT409Batch sends only. The key of one or more emails conflicts, see details.conflicts. No emails were sent.

Best practices

Generate unique keys per email. Use a combination of entity ID + action type (e.g., order-12345-confirmation, user-789-welcome). UUID v4 works well for truly unique operations.

Store keys for retry logic. If your application might retry a request, store the idempotency key so you can use the same key on retry. Generating a new key defeats the purpose.

When to use idempotency

Use CaseRecommendation
Order confirmations, receiptsAlways use idempotency
Password resets, verification emailsAlways use idempotency
Webhook-triggered emailsUse if webhook might retry
Marketing campaigns (batch)Give each email an idempotency_key, so you can retry a failed batch
Test emails during developmentSkip, duplicates are harmless

Troubleshooting

Why am I getting 'key already used' errors?

This happens when you reuse an idempotency key with a different email body. Common causes:

  • Using a static key instead of generating per-email keys
  • Changing the email content (subject, body, recipients) between retries

Solution: Generate a unique key for each distinct email. For retries, use the exact same request body.

What's the difference between 409 and 422?
  • 422 (Unprocessable Entity): Your idempotency key is invalid (empty, too long, or wrong format)
  • 409 (Conflict): The key is valid but conflicts with an existing request (different body or concurrent request)

Solution: For 422, fix the key format. For 409, either wait and retry (concurrent) or use a different key (payload mismatch).

Why do I still get a 409 after the first request failed?

The first request failed after its email may already have been sent, so Lettermint blocks the key for 24 hours to prevent a duplicate. Check whether the email was sent, for example in the dashboard. If it was not, send it again with a new key.

How long do idempotency keys last?

Keys expire 24 hours after first use. After expiration, the same key can be reused as if it were new. This prevents permanent key exhaustion while providing a reasonable retry window.

Can I use the same key across different projects?

Yes. Idempotency keys are scoped per project, so order-123 in Project A is independent from order-123 in Project B.