> ## Documentation Index
> Fetch the complete documentation index at: https://docs.lettr.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Idempotency

> Use Lettr request IDs and application-level idempotency patterns to send each email exactly once, even through failures and retries

Duplicate emails damage user trust — nobody wants to receive the same order confirmation twice. Network failures, retries, and concurrent requests can all cause a single email to be sent more than once unless your integration is designed to prevent it. This page covers how to use Lettr's request IDs and application-level idempotency patterns to ensure each email is sent exactly once, even when failures and retries occur.

## Request IDs

Every successful send returns a unique `request_id` that identifies the transmission. This ID is your primary handle for tracking the email through delivery, webhooks, and the dashboard:

```javascript theme={null}
const response = await lettr.emails.send({
  from: 'you@example.com',
  to: ['recipient@example.com'],
  subject: 'Order Confirmation',
  html: '<p>Your order is confirmed!</p>'
});

console.log(response.data.request_id);
// "7582751837467401763"
```

## Using Request IDs

Store the `request_id` alongside your business record (order, user, ticket) so you can reference it later. The request ID lets you query the email's delivery status through the API, look it up in the dashboard, trace it through webhook events, and provide it to Lettr support if you need to investigate a delivery issue.

```javascript theme={null}
// Store with your order
await db.orders.update(orderId, {
  confirmation_email_request_id: response.data.request_id
});

// Later, check delivery status
const email = await lettr.emails.get(requestId);
console.log(email.status);
```

## Preventing Duplicate Sends

The simplest way to prevent duplicates is to check a flag in your database before sending. If the email has already been sent for a given business event (order confirmed, password reset requested), skip the API call:

```javascript theme={null}
async function sendOrderConfirmation(order) {
  // Check if already sent
  if (order.confirmation_email_sent) {
    console.log('Email already sent for order', order.id);
    return;
  }

  const response = await lettr.emails.send({
    from: 'orders@example.com',
    to: [order.customer_email],
    subject: `Order #${order.id} Confirmed`,
    template_slug: 'order-confirmation',
    substitution_data: { order },
    metadata: {
      order_id: order.id,
      idempotency_key: `order-confirmation-${order.id}`
    }
  });

  // Mark as sent
  await db.orders.update(order.id, {
    confirmation_email_sent: true,
    confirmation_email_request_id: response.data.request_id
  });
}
```

## Distributed Idempotency

For distributed systems where multiple workers might process the same event, use a shared cache or database lock to coordinate. Store a unique idempotency key derived from the business event and check it before sending:

```javascript theme={null}
async function sendPasswordReset(userId, resetToken, userEmail) {
  const idempotencyKey = `password-reset-${userId}-${resetToken}`;

  // Use a distributed lock or cache to check if already sent
  const alreadySent = await cache.get(idempotencyKey);
  if (alreadySent) {
    console.log('Password reset email already sent');
    return;
  }

  const response = await lettr.emails.send({
    from: 'security@example.com',
    to: [userEmail],
    subject: 'Password Reset Request',
    template_slug: 'password-reset',
    metadata: {
      idempotency_key: idempotencyKey,
      user_id: userId
    }
  });

  // Mark as sent
  await cache.set(idempotencyKey, response.data.request_id, { ttl: 86400 });
  return response;
}
```

## Safe Retry Pattern

When a send request fails with a server error (5xx) or network timeout, it's safe to retry — but only if the error was transient. Client errors (4xx) indicate a problem with the request itself and should not be retried. This pattern implements exponential backoff for retryable errors while immediately re-throwing client errors:

```javascript theme={null}
async function sendEmailWithRetry(emailData, maxRetries = 3) {
  let lastError;

  for (let attempt = 1; attempt <= maxRetries; attempt++) {
    try {
      const response = await lettr.emails.send(emailData);
      return response;
    } catch (error) {
      lastError = error;

      // Don't retry client errors (4xx)
      if (error.status >= 400 && error.status < 500) {
        throw error;
      }

      // Wait before retrying (exponential backoff)
      if (attempt < maxRetries) {
        await sleep(Math.pow(2, attempt) * 1000);
      }
    }
  }

  throw lastError;
}
```

## Request ID in Webhooks

Every webhook event includes the `request_id` from the original send. This lets your webhook handler look up the corresponding business record (order, user, ticket) and update its status based on the delivery outcome:

```json theme={null}
{
  "type": "message.delivery",
  "data": {
    "request_id": "7582751837467401763",
    "message_id": "msg_abc123",
    "to": "recipient@example.com",
    "metadata": {
      "order_id": "order_12345"
    }
  }
}
```

## Best Practices

<AccordionGroup>
  <Accordion title="Always Store Request IDs">
    Persist the `request_id` alongside the business record that triggered the email (e.g., an order row, a user record, a support ticket). This gives you a direct lookup path when investigating delivery issues, correlating webhook events, or providing information to support.
  </Accordion>

  <Accordion title="Use Meaningful Idempotency Keys">
    Construct keys from the email's purpose and the entity it relates to, such as `order-confirmation-{order_id}` or `password-reset-{user_id}-{token}`. Avoid generic keys like `email-123` — the key should make it impossible for a different email type to collide with the same value.
  </Accordion>

  <Accordion title="Check Before Sending">
    For critical emails like order confirmations and password resets, always verify that the email hasn't already been sent before calling the API. A database flag check is fast and prevents unnecessary API calls, even before metadata-based deduplication comes into play.
  </Accordion>

  <Accordion title="Handle Timeouts Carefully">
    A network timeout does not mean the email wasn't sent — the request may have reached Lettr successfully, but the response was lost. Before retrying, query the API using your idempotency key to check whether the email was already accepted for delivery.
  </Accordion>
</AccordionGroup>

## Timeout Handling

A timeout (`ETIMEDOUT`) or connection reset (`ECONNRESET`) means your client didn't receive a response, but Lettr may have already accepted and queued the email. Before retrying, check your local idempotency records (database flag or cache) to see if the send was already recorded. If it wasn't, you can safely retry — but be aware the email may have been sent despite the timeout:

```javascript theme={null}
async function safeSend(emailData, idempotencyKey) {
  try {
    const response = await lettr.emails.send(emailData);
    await cache.set(idempotencyKey, response.data.request_id, { ttl: 86400 });
    return response;
  } catch (error) {
    if (error.code === 'ETIMEDOUT' || error.code === 'ECONNRESET') {
      // Check local cache to see if we already recorded a successful send
      const existingRequestId = await cache.get(idempotencyKey);
      if (existingRequestId) {
        return { data: { request_id: existingRequestId }, alreadySent: true };
      }
      // If not in cache, the send may or may not have succeeded.
      // Log the ambiguity and consider retrying with caution.
      console.warn('Timeout occurred, send status unknown for:', idempotencyKey);
    }
    throw error;
  }
}
```
