# Idempotency

## 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.

:::tip
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

:::info
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

### Header

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

### Key requirements

| Requirement | Value |
|-------------|-------|
| Length | 1–255 characters |
| Format | Any string (UUID v4 recommended) |
| Validity | 24 hours from first use |
| Scope | Per project |

### Request scenarios

| Scenario | Result |
|----------|--------|
| First request with new key | Email sent, response cached |
| Same key + same body (retry) | Cached response returned, no duplicate |
| Same key + different body | `409 Conflict` error |
| Same key + request in progress | `409 Conflict` error |
| Key used 24+ hours ago | Treated as new key |

## Examples

### Send with idempotency key

<CodeTabs>

```typescript title="Node.js"
import { Lettermint } from "lettermint";

const email = Lettermint.email(process.env.LETTERMINT_PROJECT_TOKEN!);

const response = await email
  .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")
  .send();

console.log(`Email sent: ${response.message_id}`);
```

```php title="PHP"
<?php

$email = Lettermint\Lettermint::email(getenv('LETTERMINT_PROJECT_TOKEN'));

$response = $email
    ->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')
    ->send();

echo "Email sent: " . $response->message_id;
```

```python title="Python"
from lettermint import Lettermint
import os

lettermint = Lettermint(api_token=os.environ["LETTERMINT_PROJECT_TOKEN"])

response = (
    lettermint.email
    .from_("John Doe <john@yourdomain.com>")
    .to("recipient@example.com")
    .subject("Order Confirmation #12345")
    .html("<p>Your order has been confirmed.</p>")
    .idempotency_key("order-12345-confirmation")
    .send()
)

print(f"Email sent: {response['message_id']}")
```

```bash title="cURL"
curl -X POST https://api.lettermint.co/v1/send \
  -H "x-lettermint-token: $LETTERMINT_PROJECT_TOKEN" \
  -H "Idempotency-Key: order-12345-confirmation" \
  -H "Content-Type: application/json" \
  -d '{
    "from": "John Doe <john@yourdomain.com>",
    "to": ["recipient@example.com"],
    "subject": "Order Confirmation #12345",
    "html": "<p>Your order has been confirmed.</p>"
  }'
```

</CodeTabs>

### Response (202 Accepted)

```json
{
  "message_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479"
}
```

### Safe retry (same key + same body)

If you retry the exact same request with the same idempotency key and body, you receive a `202 Accepted` with the same `message_id`. No duplicate email is sent.

```json
{
  "message_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479"
}
```

### Conflict (same key + different body)

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

**HTTP 409 Conflict**
```json
{
  "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**
```json
{
  "message": "Another request with the same idempotency key is currently being processed. Please retry this request later."
}
```

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

## SMTP

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

<CodeTabs>

```typescript title="Node.js (Nodemailer)"
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"
  }
});
```

```python title="Python (smtplib)"
import smtplib
import os
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart

SMTP_HOST = 'smtp.lettermint.co'
SMTP_PORT = 587
USERNAME = 'lettermint'
API_TOKEN = os.environ.get('LETTERMINT_PROJECT_TOKEN')

msg = MIMEMultipart('alternative')
msg['From'] = 'sender@yourdomain.com'
msg['To'] = 'recipient@example.com'
msg['Subject'] = 'Order Confirmation #12345'
msg['Idempotency-Key'] = 'order-12345-confirmation'

msg.attach(MIMEText('<p>Your order has been confirmed.</p>', 'html'))

with smtplib.SMTP(SMTP_HOST, SMTP_PORT) as server:
    server.starttls()
    server.login(USERNAME, API_TOKEN)
    server.send_message(msg)
```

</CodeTabs>

## Error reference

| Error Code | HTTP Status | Message |
|------------|-------------|---------|
| `invalid_idempotency_key` | 422 | The idempotency key must be between 1-255 characters. |
| `invalid_idempotent_request` | 409 | This idempotency key has already been used on a request with a different payload. |
| `concurrent_idempotent_requests` | 409 | Another request with the same idempotency key is currently being processed. |

## Best practices

:::tip
**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.
:::

:::warning
**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 Case | Recommendation |
|----------|----------------|
| Order confirmations, receipts | Always use idempotency |
| Password resets, verification emails | Always use idempotency |
| Webhook-triggered emails | Use if webhook might retry |
| Marketing campaigns (batch) | Optional, typically single-shot |
| Test emails during development | Skip, duplicates are harmless |

## Troubleshooting


<details>
<summary>Why am I getting 'key already used' errors?</summary>

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.

</details>

<details>
<summary>What's the difference between 409 and 422?</summary>

- **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).

</details>

<details>
<summary>How long do idempotency keys last?</summary>

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.

</details>

<details>
<summary>Can I use the same key across different projects?</summary>

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

</details>
