Skip to content
Docs menuCurrent page: Quickstart

Get started

Quickstart

Connect your domain, send with the Resend SDK you already use, receive catch-all mail and follow every delivery.

Tested with resend@6.30.0

Last reviewed

On this page (5)
Step 1 of 5

Connect your domain

In your project, open Domains, add a domain or a dedicated subdomain, and publish the records Letterpier shows: the ownership TXT record, DKIM, SPF, the return-path CNAME and DMARC. Letterpier checks them on its own, so the domain verifies soon after your DNS changes appear; select Check now to check straight away. A domain belongs to exactly one project.

Receiving adds an MX record
  • SPF: publish only one SPF record per name. If one exists, add include:spf.letterpier.com to it, before its all term, instead of adding a second record.
  • DMARC: keep a policy you already have. Letterpier accepts any single existing DMARC policy, and for a subdomain it also accepts the policy of the registered domain above it. If there is none, start with v=DMARC1; p=none;.
  • Reverse DNS: the PTR record is set once per sending IP, for mail.letterpier.com. There is nothing to do per domain.

SPF, DKIM and DMARC help receiving providers authenticate your mail. Delivered means the recipient’s server accepted the message. It isn’t a promise of the inbox.

Every record, and how verification works

Step 2 of 5

Send with your existing SDK

Once the domain is verified, create a live key with Sending only permission. Keep it in your product’s server environment and point the Resend SDK at Letterpier with baseUrl. Keys starting with lp_test_ are sandbox keys: they capture messages without sending them, which is the safe way to test.

lib/mail.ts

import { Resend } from 'resend';export const mail = new Resend(process.env.LETTERPIER_API_KEY, {  baseUrl: 'https://app.letterpier.com',});

send-welcome.ts

import { mail } from './lib/mail';type Customer = { id: string; email: string };export async function sendWelcome(customer: Customer) {  const { data, error } = await mail.emails.send(    {      from: 'Your product <hello@notify.yourproduct.example>',      to: customer.email,      subject: 'Welcome',      text: 'Your account is ready.',    },    // The same key within 24 hours returns the first response.    { idempotencyKey: `welcome/${customer.id}` },  );  if (error) throw new Error(`${error.name}: ${error.message}`);  return data.id; // Queued, not delivered.}

.env.local

LETTERPIER_API_KEY=lp_live_...

Pass a stable idempotencyKey when you might retry a request: within 24 hours the same key returns the first response instead of creating a second message.

A successful response means queued

Batches, scheduling, attachments and errors

Step 3 of 5

Receive catch-all mail

Turn receiving on for the domain and publish its MX record. Once the domain is verified and MX matches, mail to any address on the domain arrives in its project. Add a public HTTPS webhook subscribed to email.received, verify each request with the endpoint’s signing secret, and fetch the message with a full-access live key.

app/api/letterpier/route.ts

import type { WebhookEventPayload } from 'resend';import { mail } from '@/lib/mail';import { markProcessed, wasProcessed } from '@/lib/events';export async function POST(request: Request) {  const payload = await request.text(); // The raw body, unchanged  const id = request.headers.get('svix-id') ?? '';  let event: WebhookEventPayload;  try {    event = mail.webhooks.verify({      payload,      headers: {        id,        timestamp: request.headers.get('svix-timestamp') ?? '',        signature: request.headers.get('svix-signature') ?? '',      },      webhookSecret: process.env.LETTERPIER_WEBHOOK_SECRET!,    });  } catch {    return new Response('Invalid signature', { status: 400 });  }  // Retries repeat svix-id: skip events you've already processed.  if (await wasProcessed(id)) return new Response(null, { status: 204 });  if (event.type === 'email.received') {    const emailId = event.data.email_id;    const email = await mail.emails.receiving.get(emailId);    const files = await mail.emails.receiving.attachments.list({      emailId,    });    // Anything but a 2xx makes Letterpier try again later.    if (email.error || files.error) {      return new Response('Try again', { status: 503 });    }    // Received mail is untrusted: validate it before you act on it.    console.log(email.data.subject, files.data.data.length);  }  // Record the event only once it's handled, then answer 2xx.  await markProcessed(id);  return new Response(null, { status: 204 });}

.env.local

LETTERPIER_API_KEY=lp_live_...LETTERPIER_WEBHOOK_SECRET=whsec_...
  • Verify the raw request body, exactly as it arrived. Parsing and re-serialising it breaks the signature.
  • Delivery is at least once. Deduplicate on the svix-id header, which stays the same across retries, and record it only once the event is handled.
  • Attachment download links expire after 15 minutes. Request the attachment’s metadata again for a fresh link.
  • Treat received content and attachments as untrusted. A visible From address is not proof of who sent it.

Receiving and webhooks in detail

Step 4 of 5

Follow delivery

The Emails page of your project shows every message with its body, attachments, status, events and last error. Webhook deliveries that ran out of attempts can be replayed from the dashboard.

  • A permanent failure (Bounced or Failed) adds the recipient to the project’s suppression list, and later messages to that address aren’t sent. Find out why before you remove a suppression.
  • If the hand-off to Postal, Letterpier’s mail server, was interrupted, the message is Unknown. Letterpier never resends it on its own. Use Check Postal acceptance to look it up without sending a duplicate.

What every status means

Step 5 of 5

Choose retention

In the project’s settings, keep mail for 1 to 90 days; the default is 30. Lowering it also applies to messages you already have. Raising it doesn’t bring back anything already deleted.

  • Deleting a message removes its content, attachments and events, and the matching copy in Postal.
  • Postal keeps its own copies for a shorter time: raw mail for 7 days, metadata for 30 days.
  • Backups keep 7 daily recovery points, so deleted mail leaves them as they expire. One exception: a copy of the backup repository made on 29 September 2026 is kept on the operator’s own computer. It doesn’t expire automatically, so it keeps the data of that day until it is deleted or replaced.