Skip to main content
A send request can fail on invalid parameters, an unverified domain, a rate limit, or a transient server problem, and each of those wants a different response from the caller. This page covers the error format, the codes worth handling by name, which responses are safe to retry, and a retry pattern that respects Retry-After and the Idempotency-Key header.

Error response format

Every error is a JSON object with the same shape. error_code identifies the category of failure, message is a human-readable description, and errors (present on validation failures only) maps each invalid field to its problems:

Common error codes

Validation errors

A 422 means the request was well-formed but carried invalid data: a missing required field, a malformed recipient address, or a subject over the length limit. The errors object names the field, so a handler can report the specific problem:
An unverified or misconfigured sending domain is not a validation error. It returns 400 with invalid_domain or unconfigured_domain, so a handler scoped to 422 misses the most common first-send failure. SDKs that map only 422 to a validation exception (lettr-php raises a generic ApiException here) will not route it to a validation catch block either. See Domain errors below.

Domain errors

The from address must use a verified domain with its DNS records in place. These are the most common errors during initial setup, and they are not retryable until the domain is fixed:

Retryable and non-retryable errors

Retrying a validation error or an unauthorized request produces the same result every time, so the retry path should only see server failures, rate limits, and an idempotent request that is still in flight. Two 409 codes exist only for requests that carry an Idempotency-Key; the Idempotency guide covers the header itself.

Retry logic

A retry helper needs three things: a stable Idempotency-Key so a retried send cannot deliver twice, a check that rejects non-retryable errors before backing off, and a delay that honours Retry-After when the response carries one. Without that header the pattern below sleeps a random interval under a capped exponential ceiling, so clients recovering from the same outage do not retry in lockstep, and it never sleeps after the final attempt:
A network timeout is retryable with the same key and payload. If the first attempt reached Lettr, the retry returns the original result with Idempotency-Replayed: true; if it did not, the retry sends the email.

Handling the result

Wrapping the retry helper gives application code a structured result instead of an exception. Log enough context to debug later, and map the codes the caller can act on:

Soft bounce retries

Retries on the API side end once Lettr accepts the message. From there, Lettr retries soft bounces from the receiving mail server itself, so the integration needs no logic for them. Hard bounces are permanent and are never retried:

Circuit breaker pattern

In high-volume sending, a run of failures usually means a systemic problem rather than a bad request. A circuit breaker stops sending after a threshold of failures, waits out a cooldown, then lets a single test request through before resuming normal traffic:

Monitoring errors

Failures after the API accepts an email never surface in the send response. A webhook handler for bounce and generation-failure events is the place to catch them: