Sending email
Every field of POST /v1/emails, the limits per email, custom headers and safe retries.
POST /v1/emails queues one email. It needs a key with sending_access or full_access.
Fields
| Field | Type | Rules |
|---|---|---|
from |
address | Required. A verified domain of your account. |
to |
address or array | Required, at least one address. |
cc, bcc |
address or array | Optional. |
replyTo |
address or array | Optional, one address or several. |
subject |
string | Required, one line, at most 998 characters. |
html, text |
string | At least one of them. |
tags |
object | Up to 10. Names use letters, digits, - and _ (at most 64), values at most 256 characters. Tags come back on the email and in webhook events. |
headers |
object | Up to 20 extra headers, see below. |
attachments |
array | See Attachments and inline images. |
scheduledAt |
date-time | Optional. Send later, see Scheduled send. |
to, cc and bcc hold at most 50 recipients together, and the whole email at most 15 MB as sent: the MIME message letr.dev builds, with its encoding, headers and DKIM signature. That leaves room for about 10 MB of attachments.
Display names
Every address can carry a display name: "Ada Lovelace <[email protected]>". The plain form "[email protected]" works too.
{
"from": "Acme Billing <[email protected]>",
"to": ["Ada Lovelace <[email protected]>", "[email protected]"],
"replyTo": "Acme Support <[email protected]>",
"subject": "Invoice 1042",
"html": "<p>Your invoice is attached.</p>",
"text": "Your invoice is attached.",
"tags": { "type": "invoice" }
}
Custom headers
headers adds fields such as In-Reply-To and References for threading, List-Unsubscribe, or X-Entity-Ref-ID. Names must be RFC 5322 field names, values at most 998 characters without line breaks, and each name may appear once.
letr.dev sets these itself and refuses them with 422 reserved_header: From, Sender, To, Cc, Bcc, Reply-To, Subject, Date, Message-ID, Return-Path, MIME-Version, DKIM-Signature, Received, Feedback-ID, every Content-* and every X-Letr-* header.
Retries without duplicates
Send an Idempotency-Key header (at most 255 characters) and retry with the same key after a timeout. A repeat with the same key and the same body returns the first email's id with its current status and sends nothing new. The same key with a different body answers 422 idempotency_key_reused. letr.dev remembers a key for at least 32 days and frees it within a day after that; then the same key sends a new email.
curl https://api.letr.dev/v1/emails \
-H "Authorization: Bearer $LETR_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: invoice-1042" \
-d @email.json
Test recipients
Send to these addresses to try letr.dev without reaching a real mailbox:
[email protected]: sent, then delivered.[email protected]: sent, then bounced (hard,550 5.1.1); suppresses nothing.[email protected]: sent, delivered, then complained; suppresses nothing.[email protected]: refused as if the address were on the suppression list (422 recipient_suppressed).
A label may follow a plus sign, for example [email protected], to tell your tests apart.
When every recipient is a test address, the email never leaves letr.dev. It may use any from address at a domain of your account or at test.letr.dev, and does not count toward usage, the monthly ceiling, the daily limit or the bounce and complaint rates. Its events and webhooks are real. When an email mixes test and real recipients, the normal rules apply to the whole email. Any other address at test.letr.dev is refused with 422 invalid_recipients.
Scheduled send
Set scheduledAt to send later: at least 1 minute and at most 7 days ahead, otherwise the answer is 422 invalid_scheduled_at. The email is created with status scheduled and counts toward usage when it is sent. If the account is paused or disabled when it comes due, it fails unsent and does not count. An email to test recipients only cannot be scheduled.
{
"from": "Acme <[email protected]>",
"to": "[email protected]",
"subject": "Your trial ends tomorrow",
"text": "Your trial ends tomorrow.",
"scheduledAt": "2026-11-03T08:00:00Z"
}
Until it is sent you can cancel it with POST /v1/emails/{id}/cancel, using a full_access key or the sending key that created it. The answer is the email with status canceled. Once it is no longer waiting, cancelling answers 409 not_scheduled.
curl -X POST https://api.letr.dev/v1/emails/$ID/cancel \
-H "Authorization: Bearer $LETR_API_KEY"
Reading emails
GET /v1/emails/{id} answers one email with its status and events. GET /v1/emails lists the account's emails, newest first, within the message history: status filters by one or more statuses (scheduled, canceled, queued, sent, delivered, deferred, bounced, complained, failed, comma-separated), q matches the email id or a part of a recipient or the subject (3 to 200 characters), limit is 1 to 100 (default 50), and the answer's nextCursor goes into cursor for the next page. The list needs a read_only or full_access key.
GET /v1/emails/{id}/content answers what letr.dev sent: the headers, text, html and the list of attachments; GET /v1/emails/{id}/content/attachments/{index} streams the bytes of one attachment, index being its position in that list. Content is kept, encrypted at rest, as long as the email is in the history, unless a recipient erasure deletes it earlier. A sending_access key reads only single emails it sent (the email, its content and attachments) and cannot list; read_only and full_access read every email of the account.
curl "https://api.letr.dev/v1/emails?status=bounced,failed&limit=20" \
-H "Authorization: Bearer $LETR_API_KEY"
Answers
| Status | When |
|---|---|
202 |
Stored and queued; the body has id and status. |
400 |
The body is not readable JSON, or the Idempotency-Key is longer than 255 characters. |
401 |
No key or an unknown, revoked or expired key. |
403 |
Account not active or paused, key without permission, or a from-domain the key may not use. |
422 |
A field breaks a rule above, the domain is not verified, or a recipient is suppressed. |
429 |
Your monthly ceiling or the daily limit is reached, or too many requests. |
Errors lists every code.