Guides
Receiving
Turn receiving on for a verified domain and mail to any address on it arrives in its project. Fetch the body, attachments and original message with a full-access live key.
Last reviewed
On this page (7)
Receiving is optional, and off for a new domain. Once you turn it on and the domain is verified, Letterpier’s mail server accepts mail for any address on it: support@, reply+4711@, anything. Each message goes to the project that owns the domain and appears there as Received.
- Routing uses the SMTP envelope recipient (
RCPT TO), not the visible To header, which the sender can set to anything. - The body, the original message (.eml) and every attachment are stored encrypted at rest. This is encryption at rest with keys the application manages. It is not end-to-end encryption. Subjects, addresses and event metadata stay in plaintext so they can be searched, and Postal, our mail server, keeps the raw message in plaintext in its private database for 7 days.
- Received messages can be up to about 14 MB, including attachments; larger messages are refused and not stored.
- Deliveries from the mail server are deduplicated, so a retried hand-off never creates a second message.
- When incoming mail fails, no failure bounce goes back to the sender, so forged senders don’t receive backscatter.
Receiving needs one MX record at the domain’s name. It is only required while receiving is on for the domain, and it never holds up sending: a sending-only domain doesn’t publish it. Turn receiving on when you add the domain, or later on the domain’s page in the dashboard; through the API, pass receiving: true when you create the domain. Receiving shows as enabled once the domain is verified and the MX record matches.
| Type | Name | Value | Priority |
|---|---|---|---|
| MX | notify. | mail. | 10 |
The receiving record
- Type
- MX
- Name
notify.yourproduct. example - Value
mail.letterpier. com - Priority
- 10
yourproduct.example, point a subdomain such as notify.yourproduct.example at Letterpier instead, and leave the existing mailboxes where they are.When a message arrives, subscribed endpoints receive a signed email.received event. It carries the message ID and enough to decide what to do next; fetch the content with the ID.
{ "type": "email.received", "created_at": "2026-09-29T09:14:05.118Z", "data": { "email_id": "0f6d2c1e-5a7b-4c0e-9d7e-3b1f8a2c4d60", "created_at": "2026-09-29T09:14:04.902Z", "from": "Ada Lovelace <ada@customer.example>", "to": ["support@notify.yourproduct.example"], "subject": "Question about my invoice", "sandbox": false, "attachments": [ { "id": "7c2e9b14-0d3a-4f6b-8e21-5a9c0f1d2b73", "filename": "screenshot.png", "content_type": "image/png" } ], "message_id": "<CAF=abc123@mail.customer.example>" }}to holds the envelope recipient. attachments lists the files without their content. message_id is the sender’s Message-ID header, when there is one.
Fetch a received message with mail.emails.receiving.get(id) (GET /emails/receiving/:id) and a full-access live key. Sandbox keys and sending-only keys are refused with 403 restricted_api_key.
const { data: email, error } = await mail.emails.receiving.get(emailId);email?.subject; // As receivedemail?.text; // Plain-text body, or nullemail?.html; // HTML body, or nullemail?.raw; // Letterpier extension: a signed link to the original .emlThe response has the sender, the envelope recipient, the subject, both bodies, the attachments and raw: a signed link to the original .eml, byte for byte as it arrived. Like every download link, it expires after 15 minutes.
headers, message_id or received_for, and cc, bcc and reply_to are always empty. Read them from the original .eml in raw.mail.emails.receiving.attachments.list({ emailId }) returns every attachment’s metadata; .get({ emailId, id }) returns one. Each has an id, filename, size in bytes, content_type, content_id, and a signed download_url with its expires_at.
const { data: list } = await mail.emails.receiving.attachments.list({ emailId });for (const attachment of list?.data ?? []) { // Links expire after 15 minutes. Ask again for a fresh one. const { data: fresh } = await mail.emails.receiving.attachments.get({ emailId, id: attachment.id, }); const response = await fetch(fresh!.download_url); const bytes = new Uint8Array(await response.arrayBuffer()); console.log(attachment.filename, bytes.byteLength);}- Download links expire after 15 minutes. Request the metadata again for a new link.
- Downloads arrive as a file (
Content-Disposition: attachment), never rendered in a browser. - Links stop working once the message is deleted, by you or by retention.
mail.emails.receiving.list() (GET /emails/receiving) returns received messages, newest first, with the same full-access live key.
const { data } = await mail.emails.receiving.list();data?.data; // The latest 100 received messages, newest firstdata?.has_more; // true when there may be older oneslimit, after and before have no effect. Use the email.received webhook to catch every message.Everything in a received message was written by someone outside your control.
- A visible From address is not proof of identity. Letterpier verifies that the message came from its own mail server, not what the sender claims.
- Attachments aren’t scanned for malware; attachment scanning is not yet available. Never open or run them with privileges.
- In the dashboard, previews run in a sandbox that blocks scripts, forms and remote content.
- Don’t pass received content to tools or automations with broad permissions without validating it first.