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

# Errors & Retries

> Understand Lettr API error responses and codes, learn which failures are safe to retry, and build resilient send logic with backoff

API calls can fail for many reasons — invalid parameters, unverified domains, rate limits, or transient server issues. How your integration handles these failures determines whether emails are silently lost or reliably delivered. This page covers Lettr's error response format, common error codes, which errors are safe to retry, and patterns for building resilient send logic.

## Error Response Format

All API errors return a JSON object with a consistent structure. The `error_code` field identifies the category of failure, `message` provides a human-readable description, and `errors` (present on validation failures) lists problems for specific fields:

```json theme={null}
{
  "error_code": "validation_error",
  "message": "The given data was invalid.",
  "errors": {
    "to": ["The to field is required."],
    "subject": ["The subject must not exceed 998 characters."]
  }
}
```

## Common Error Codes

| Error Code            | HTTP Status | Description                                |
| --------------------- | ----------- | ------------------------------------------ |
| `validation_error`    | 422         | Invalid request parameters                 |
| `invalid_domain`      | 400         | Sender domain not verified                 |
| `unconfigured_domain` | 400         | Domain not ready for sending               |
| `template_not_found`  | 404         | Template slug not found                    |
| `send_error`          | 500         | Email sending error                        |
| `transmission_failed` | 502         | Email delivery to upstream provider failed |

## Validation Errors

Validation errors (`422`) mean the request was well-formed but contained invalid data — a missing required field, an unverified sender domain, or a subject line exceeding the character limit. The `errors` object in the response maps each invalid field to an array of error messages:

```javascript theme={null}
try {
  await lettr.emails.send(emailData);
} catch (error) {
  if (error.status === 422) {
    console.log('Validation errors:', error.errors);

    // Handle specific field errors
    if (error.errors.to) {
      console.log('Invalid recipients:', error.errors.to);
    }
    if (error.errors.from) {
      console.log('Invalid sender:', error.errors.from);
    }
  }
}
```

## Domain Errors

Domain-related errors occur when the `from` address uses a domain that hasn't been verified or whose DNS records aren't properly configured. These are the most common errors during initial integration setup:

```javascript theme={null}
try {
  await lettr.emails.send({
    from: 'you@unverified-domain.com',
    to: ['recipient@example.com'],
    subject: 'Hello',
    html: '<p>Hello!</p>'
  });
} catch (error) {
  if (error.error_code === 'invalid_domain') {
    console.log('Domain not verified. Please verify your domain first.');
  }
  if (error.error_code === 'unconfigured_domain') {
    console.log('Domain DNS records not configured correctly.');
  }
}
```

## Retry Logic

A well-designed retry function should distinguish between retryable errors (server failures, rate limits) and non-retryable errors (validation, authentication). The following pattern uses exponential backoff and respects the `retry_after` value from rate limit responses:

```javascript theme={null}
async function sendEmailWithRetry(emailData, options = {}) {
  const {
    maxRetries = 3,
    baseDelay = 1000,
    maxDelay = 30000
  } = options;

  let lastError;

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

      // Don't retry client errors (except rate limits)
      if (error.status >= 400 && error.status < 500 && error.status !== 429) {
        throw error;
      }

      // Calculate delay with exponential backoff
      const delay = Math.min(
        baseDelay * Math.pow(2, attempt - 1),
        maxDelay
      );

      // Use retry-after header if provided
      const retryAfter = error.retryAfter
        ? error.retryAfter * 1000
        : delay;

      console.log(`Attempt ${attempt} failed. Retrying in ${retryAfter}ms...`);
      await sleep(retryAfter);
    }
  }

  throw lastError;
}
```

## Retryable vs Non-Retryable Errors

Not all errors should be retried. Retrying a validation error or an unauthorized request will produce the same result every time. The table below summarizes which errors are safe to retry and what action to take for each:

| Error Type                   | Retryable | Action                         |
| ---------------------------- | --------- | ------------------------------ |
| `429` Rate limit             | Yes       | Wait and retry with backoff    |
| `500` Server error           | Yes       | Retry with backoff             |
| `502/503/504` Gateway errors | Yes       | Retry with backoff             |
| `401` Unauthorized           | No        | Check API key                  |
| `403` Forbidden              | No        | Check API key permissions      |
| `422` Validation             | No        | Fix request data               |
| `404` Not found              | No        | Check template/resource exists |

## Error Handling Best Practices

Wrapping the retry function with error-specific handling gives you a clean interface for your application code. Log errors with enough context to debug later, and return structured results so calling code can decide how to proceed:

```javascript theme={null}
async function sendEmail(emailData) {
  try {
    const response = await sendEmailWithRetry(emailData);
    return { success: true, data: response.data };
  } catch (error) {
    // Log error for debugging
    console.error('Email send failed:', {
      error_code: error.error_code,
      message: error.message,
      to: emailData.to,
      timestamp: new Date().toISOString()
    });

    // Handle specific errors
    switch (error.error_code) {
      case 'validation_error':
        return { success: false, error: 'Invalid email data', details: error.errors };

      case 'invalid_domain':
        return { success: false, error: 'Sender domain not verified' };

      case 'rate_limit_exceeded':
        return { success: false, error: 'Rate limit exceeded', retryAfter: error.retryAfter };

      default:
        return { success: false, error: 'Failed to send email' };
    }
  }
}
```

## Soft Bounce Retries

After Lettr accepts an email for delivery, the receiving mail server may reject it with a temporary error (soft bounce). Lettr automatically retries soft bounces on your behalf — you don't need to implement retry logic for these. Hard bounces are permanent failures and are never retried:

| Bounce Type                   | Auto-Retry | Max Attempts |
| ----------------------------- | ---------- | ------------ |
| Soft bounce (mailbox full)    | Yes        | 3            |
| Soft bounce (server error)    | Yes        | 3            |
| Hard bounce (invalid address) | No         | -            |

## Circuit Breaker Pattern

In high-volume sending scenarios, repeated failures to the API can indicate a systemic issue (service degradation, network problem). A circuit breaker stops sending attempts after a threshold of failures, waits for a cooldown period, then allows a single test request through. If the test succeeds, normal traffic resumes. This prevents your application from wasting resources on requests that are likely to fail:

```javascript theme={null}
class EmailCircuitBreaker {
  constructor(threshold = 5, resetTimeout = 60000) {
    this.failures = 0;
    this.threshold = threshold;
    this.resetTimeout = resetTimeout;
    this.state = 'closed';
    this.lastFailure = null;
  }

  async send(emailData) {
    if (this.state === 'open') {
      if (Date.now() - this.lastFailure > this.resetTimeout) {
        this.state = 'half-open';
      } else {
        throw new Error('Circuit breaker is open');
      }
    }

    try {
      const result = await lettr.emails.send(emailData);
      this.onSuccess();
      return result;
    } catch (error) {
      this.onFailure();
      throw error;
    }
  }

  onSuccess() {
    this.failures = 0;
    this.state = 'closed';
  }

  onFailure() {
    this.failures++;
    this.lastFailure = Date.now();
    if (this.failures >= this.threshold) {
      this.state = 'open';
    }
  }
}
```

## Monitoring Errors

Beyond handling errors in your send logic, set up webhook-based monitoring to catch delivery failures that occur after the API accepts the email. This lets you alert your team about bounces, failures, and other delivery problems in real time:

```javascript theme={null}
// Webhook handler for delivery failures
app.post('/webhooks/lettr', (req, res) => {
  const event = req.body;

  if (event.type === 'generation.generation_failure' || event.type === 'message.bounce') {
    // Alert your team
    alertTeam({
      type: event.type,
      email_id: event.data.email_id,
      recipient: event.data.to,
      error: event.data.error_message
    });
  }

  res.sendStatus(200);
});
```
