Skip to main content
Lettr allows you to receive and process incoming emails programmatically. By configuring an inbound domain and setting up webhooks, your application can capture replies to transactional emails, process support requests, parse incoming data, and trigger automated workflows whenever an email arrives.

What You Can Build

  • Support Ticket Systems - Automatically create tickets from customer emails
  • Reply Tracking - Capture replies to your transactional emails and thread conversations
  • Email-to-Action Workflows - Trigger processes when specific emails arrive
  • Data Extraction - Parse structured data from incoming emails (orders, receipts, notifications)
  • Email Forwarding - Receive, process, and forward emails with transformations
  • Unsubscribe Processing - Handle unsubscribe requests via email

How It Works

Lettr’s inbound email processing follows a four-step flow. You configure DNS to route emails to Lettr’s mail servers, and Lettr parses each email into structured data and delivers it to your application via webhook.
1

Configure Inbound Domain

Add an inbound domain in your Lettr dashboard and configure MX records to route incoming emails to Lettr’s mail servers.
2

Set Up Webhook

Create a webhook endpoint in your application to receive parsed email data when emails arrive.
3

Receive and Process

When someone sends an email to your inbound domain, Lettr parses it and sends the structured data to your webhook.
4

Build Your Logic

Route emails, extract data, trigger workflows, and respond programmatically.
Sender → MX Records → Lettr Mail Servers → Parse & Extract → Webhook → Your Application
Lettr handles the low-level SMTP reception, MIME parsing, attachment extraction, and spam scoring. Your application receives clean, structured JSON with the email’s sender, recipients, subject, body (both plain text and HTML), headers, and attachment URLs.

Quick Start

1. Add an Inbound Domain

Navigate to DomainsInbound in your dashboard, click Add Domain, and enter your domain (e.g., mail.example.com).
Inbound domain management is available through the Lettr dashboard. There is no public API endpoint for creating or managing inbound domains.

2. Configure DNS

Add the provided MX records to your domain:
TypePriorityValue
MX10rx1.sparkpostmail.com
MX10rx2.sparkpostmail.com
MX10rx3.sparkpostmail.com
Multiple MX records with equal priority provide load balancing — sending servers distribute email across all three servers. If one is unavailable, the others handle delivery. See the Setup Guide for DNS propagation details.

3. Handle Incoming Emails

Set up a webhook endpoint to receive parsed emails:
import express from 'express';

const app = express();

app.post('/webhooks/inbound', (req, res, next) => {
  // Verify webhook credentials (Basic Auth configured in Lettr dashboard)
  const authHeader = req.headers.authorization;

  if (!authHeader || !authHeader.startsWith('Basic ')) {
    return res.sendStatus(401);
  }

  const credentials = Buffer.from(authHeader.slice(6), 'base64').toString();
  const [username, password] = credentials.split(':');

  if (username !== process.env.WEBHOOK_USERNAME || password !== process.env.WEBHOOK_PASSWORD) {
    return res.sendStatus(401);
  }

  next();
}, express.json(), async (req, res) => {
  const events = req.body;

  for (const event of events) {
    const relayMessage = event.msys?.relay_message;
    if (!relayMessage) continue;

    const { msg_from, rcpt_to, content } = relayMessage;

    await processIncomingEmail({
      sender: msg_from,
      recipient: rcpt_to,
      subject: content.subject,
      body: content.text || content.html,
      headers: content.headers
    });
  }

  res.sendStatus(200);
});
Always verify webhook credentials before processing incoming emails to ensure requests are genuinely from Lettr. Configure Basic Auth or OAuth 2.0 authentication when creating your webhook in the dashboard.

Webhook Payload

When an email is received, Lettr sends a relay.relay_delivery webhook event. The payload is delivered as a JSON array of event objects, each containing a relay message with the parsed email:
[
  {
    "msys": {
      "relay_message": {
        "msg_from": "sender@example.com",
        "rcpt_to": "support@mail.example.com",
        "friendly_from": "John Sender <sender@example.com>",
        "customer_id": "1234",
        "webhook_id": "wh_abc123",
        "content": {
          "html": "<p>HTML content of the email...</p>",
          "text": "Plain text content of the email...",
          "subject": "Help with my order",
          "headers": [
            { "Message-ID": "<abc123@example.com>" },
            { "Date": "Mon, 15 Jan 2024 10:30:00 +0000" },
            { "In-Reply-To": "<original@example.com>" }
          ],
          "to": ["support@mail.example.com"],
          "email_rfc822": "...raw MIME content..."
        }
      }
    }
  }
]
Lettr automatically handles MIME parsing and delivers both the structured content fields and the raw RFC 822 message. For a complete breakdown of each field and how to work with them, see Email Parsing.

Key Capabilities

Email Routing

Route incoming emails to different handlers based on recipient, content, or custom rules:
app.post('/webhooks/inbound', express.json(), async (req, res) => {
  for (const event of req.body) {
    const relay = event.msys?.relay_message;
    if (!relay) continue;

    const recipient = relay.rcpt_to.toLowerCase();
    const { msg_from, content } = relay;

    if (recipient.startsWith('support@')) {
      await createSupportTicket({ from: msg_from, subject: content.subject, body: content.text });
    } else if (recipient.startsWith('sales@')) {
      await notifySalesTeam({ from: msg_from, subject: content.subject, body: content.text });
    } else if (recipient.match(/^reply\+/)) {
      const ticketId = recipient.match(/^reply\+([^@]+)@/)[1];
      await addReplyToTicket(ticketId, { from: msg_from, body: content.text });
    }
  }

  res.sendStatus(200);
});
The reply+ pattern above is called variable addressing (or plus addressing). It’s a common way to encode context in the recipient address so you can route replies back to the right record. See Routing for more patterns including routing by subject, sender domain, and content.

Spam Filtering

Every email includes a spam score, and you can configure automatic filtering:
// Filter based on spam score from your own analysis
if (spamScore >= 6) {
  await quarantineAsSpam(email);
  return;
}
You can also configure spam filtering sensitivity for your inbound domain through the dashboard under DomainsInbound. Regardless of dashboard settings, you can implement your own filtering logic in your webhook handler. See Spam Filtering for advanced detection patterns and allowlist/blocklist management.

Attachment Handling

Attachments are automatically extracted and made available via secure URLs:
for (const attachment of email.attachments) {
  const response = await fetch(attachment.url);
  const buffer = await response.arrayBuffer();
  await saveToStorage(attachment.filename, Buffer.from(buffer));
}
Attachment URLs expire after 24 hours. Download and store attachments promptly if you need them longer.
Each attachment includes filename, contentType, size (in bytes), and a download url. See Attachments for storage examples (S3, GCS) and security best practices including file type verification and malware scanning.

Reply Tracking

Track replies to your transactional emails by using variable reply-to addresses:
// When sending an email
await lettr.emails.send({
  from: 'support@example.com',
  to: ['customer@example.com'],
  subject: 'Your support request',
  html: content,
  reply_to: `reply+ticket_${ticketId}@mail.example.com`
});

// When receiving the reply
const match = recipient.match(/^reply\+ticket_([^@]+)@/);
if (match) {
  const ticketId = match[1];
  await addReplyToTicket(ticketId, emailData);
}
For more advanced threading, use email headers like Message-ID, In-Reply-To, and References to build full conversation threads. See Reply Tracking for header-based threading, auto-reply detection, and a complete support ticket example.

Real-World Examples

Support Ticket System

Create support tickets from incoming emails and track the conversation thread:
app.post('/webhooks/inbound', express.json(), async (req, res) => {
  for (const event of req.body) {
    const relay = event.msys?.relay_message;
    if (!relay) continue;

    const recipient = relay.rcpt_to.toLowerCase();
    const { msg_from, friendly_from, content } = relay;

    // Check if this is a reply to an existing ticket
    const replyMatch = recipient.match(/^reply\+ticket_([^@]+)@/);

    if (replyMatch) {
      // Add reply to existing ticket
      const ticketId = replyMatch[1];
      await db.ticketReplies.create({
        ticketId,
        from: msg_from,
        body: content.text
      });
    } else {
      // Create new ticket
      const ticket = await db.tickets.create({
        from: msg_from,
        fromName: friendly_from,
        subject: content.subject,
        body: content.text,
        status: 'open'
      });

      // Send acknowledgment with reply-to address for threading
      await lettr.emails.send({
        from: 'support@example.com',
        to: [msg_from],
        subject: `Re: ${content.subject}`,
        html: '<p>We received your request and will respond shortly.</p>',
        reply_to: `reply+ticket_${ticket.id}@mail.example.com`
      });
    }
  }

  res.sendStatus(200);
});

Email-to-Task Automation

Parse incoming emails to create tasks in your project management system:
app.post('/webhooks/inbound', express.json(), async (req, res) => {
  for (const event of req.body) {
    const relay = event.msys?.relay_message;
    if (!relay) continue;

    const { msg_from, content } = relay;

    // Create task from email
    const task = await projectManager.createTask({
      title: content.subject,
      description: content.text,
      reporter: msg_from
    });

    // Confirm task creation
    await lettr.emails.send({
      from: 'tasks@example.com',
      to: [msg_from],
      subject: `Task created: ${content.subject}`,
      html: `<p>Task <strong>${task.id}</strong> has been created from your email.</p>`
    });
  }

  res.sendStatus(200);
});

Using Subdomains

We recommend using a subdomain for inbound email to keep it separate from your primary email:
SubdomainUse Case
mail.example.comGeneral inbound processing
reply.example.comReply tracking for transactional emails
support.example.comCustomer support emails
inbound.example.comAutomated workflow triggers
This lets you keep your root domain’s MX records pointing to your regular email provider while routing specific subdomains through Lettr.

Processing Best Practices

  • Respond quickly — Return a 200 status from your webhook endpoint within a few seconds. If processing is complex, accept the webhook and process asynchronously via a queue.
  • Handle duplicates — Webhooks may be delivered more than once. Use the event id to deduplicate.
  • Store before processing — Save the raw webhook payload before doing any processing, so you can replay events if something fails.
  • Validate attachments — Verify file types and scan for malware before processing attachments from unknown senders.
See Best Practices for architecture patterns, reliability strategies, and monitoring recommendations.

Learn More