Skip to main content

Overview

Inbound routes allow you to receive emails and process them programmatically via webhooks. Every inbound route gets a unique email address, and you can optionally configure custom domains for branded receiving addresses.
Inbound routes are available on Starter plans and above. Check your plan’s feature availability in the dashboard.

How Inbound Mail Works

  1. Email arrives at your inbound route address
  2. Spam filtering checks the message against your configured threshold
  3. Email parsing extracts headers, body, attachments, and metadata
  4. Webhook delivery sends the complete email data to your endpoint
  5. Your application processes the email and takes action

Getting Started

Step 1: Create an Inbound Route

  1. Navigate to your project in the dashboard
  2. Go to RoutesCreate Route
  3. Select Inbound as the route type
  4. Configure your spam filtering preferences
  5. Save the route

Step 2: Get Your Inbound Address

Once created, your route receives a unique email address:
{route-uuid-without-dashes}@inbound.lettermint.co
Example: a1b2c3d4e5f67890abcdef1234567890@inbound.lettermint.co
You can find your inbound address on the route details page in your dashboard. Copy it to start receiving emails immediately.

Step 3: Configure Webhooks

To process received emails, set up a webhook endpoint:
  1. In your route settings, go to Webhooks
  2. Create a new webhook
  3. Enter your endpoint URL
  4. Save and test the webhook
See Webhook Events for the complete payload structure.

Custom Domains

Use your own domain for inbound email addresses instead of the default @inbound.lettermint.co domain.

Setting Up a Custom Domain

1

Configure custom domain

In your inbound route settings, enter your desired custom domain (e.g., support.acme.com)
2

Add MX record

Add an MX record to your DNS pointing to Lettermint’s inbound servers:
TypeHostnamePriorityValue
MXsupport.acme.com10inbound.lettermint.co
3

Verify domain

Return to your route settings and click Verify MX Record. Verification typically completes within minutes.
4

Start receiving

Once verified, emails sent to anything@support.acme.com will be routed to your webhook.
Custom domain MX records must point exclusively to Lettermint. If you need to receive emails at the same domain through multiple providers, use a subdomain like inbound.yourdomain.com.

DNS Propagation

DNS changes can take time to propagate globally:
# Check if your MX record is set correctly
dig MX support.acme.com

# Expected output should show:
# support.acme.com.  300  IN  MX  10 inbound.lettermint.co.
Troubleshooting MX verification:
  • DNS changes typically propagate in 5-60 minutes
  • Some DNS providers cache records for up to 24 hours
  • Ensure your MX record points to inbound.lettermint.co
  • Check that the hostname is correct (not doubled like support.acme.com.acme.com)

Spam Filtering

Lettermint automatically scans all inbound emails using Rspamd spam filtering. You can configure how strictly emails are filtered.

Spam Threshold

The spam threshold determines what happens to messages based on their spam score:
threshold
number
default:"5.0"
Spam score threshold. Messages with a higher score are marked as spam.
  • 5.0 (default): Standard filtering, rejects obvious spam
  • 10.0: Lenient, allows most emails through
  • 2.0: Strict, may flag legitimate emails as spam
  • null: Spam filtering OFF, accepts all emails, but will still scan and report spam scores
Setting the threshold to null (OFF) will accept all emails including obvious spam. Only use this for testing or when you implement your own spam filtering.

Spam Detection Results

Every inbound webhook includes spam scoring information:
{
  "is_spam": false,
  "spam_score": 2.4,
  "spam_symbols": [
    {
      "name": "DKIM_VALID",
      "score": -0.1,
      "options": [],
      "description": "Message has valid DKIM signature"
    },
    {
      "name": "SPF_PASS",
      "score": -0.1,
      "options": [],
      "description": "SPF check passed"
    },
    {
      "name": "BAYES_HAM",
      "score": -3.0,
      "options": [],
      "description": "Bayesian classifier identified message as non-spam"
    }
  ]
}
Your application can decide how to handle emails based on these values.

Subaddress Support

Lettermint supports email subaddressing (also known as plus addressing) for routing and filtering:
johndoemail+lm@support.acme.com
└──┬──┘└┬┘
    user    tag

Use Cases

Support ticket categorization:
support+billing@acme.com  → Route to billing team
support+technical@acme.com → Route to tech support
User identification:
orders+user123@acme.com → Link to user account #123
A/B testing email addresses:
newsletter+variant-a@acme.com
newsletter+variant-b@acme.com

Webhook Payload

Subaddresses are parsed and included in the webhook payload:
{
  "from": {
    "email": "customer@example.com",
    "subaddress": null
  },
  "recipient": "support+billing@acme.com",
  "subaddress": "billing"
}

Webhook Payload

When an email is received, Lettermint sends a message.inbound webhook with complete email data.

Example Payload

{
  "id": "abcdf1234-asdf",
  "event": "message.inbound",
  "created_at": "2025-10-02T14:30:00.000Z",
  "data": {
    "route": "support-inbox",
    "message_id": "msg_a1b2c3d4",
    "from": {
      "email": "customer@example.com",
      "name": "John Doe",
      "subaddress": null
    },
    "to": [
      {
        "email": "support@acme.com",
        "name": null,
        "subaddress": null
      }
    ],
    "cc": [],
    "recipient": "support@acme.com",
    "subaddress": null,
    "reply_to": "customer@example.com",
    "subject": "Question about my order",
    "date": "2025-10-02T14:30:00.000Z",
    "body": {
      "text": "Hi, I have a question...",
      "html": "<p>Hi, I have a question...</p>"
    },
    "tag": null,
    "headers": [
      {
        "name": "Message-ID",
        "value": "<abc123@mail.example.com>"
      }
    ],
    "attachments": [
      {
        "filename": "receipt.pdf",
        "content": "JVBERi0xLjQKJ...",
        "content_type": "application/pdf",
        "size": 45678,
        "content_id": null
      }
    ],
    "is_spam": false,
    "spam_score": 1.2,
    "spam_symbols": [
      {
        "name": "DKIM_VALID",
        "score": -0.1,
        "options": [],
        "description": "Message has valid DKIM signature"
      },
      {
        "name": "SPF_PASS",
        "score": -0.1,
        "options": [],
        "description": "SPF check passed"
      }
    ]
  }
}

Payload Fields

FieldTypeDescription
routestringRoute slug/identifier
message_idstringUnique message identifier
from.emailstringSender email address
from.namestring|nullSender display name
from.subaddressstring|nullParsed subaddress from sender
toarrayList of TO recipients
ccarrayList of CC recipients
recipientstringPrimary recipient (envelope TO)
subaddressstring|nullParsed subaddress from recipient
reply_tostring|nullReply-To address
subjectstringEmail subject
datestringISO 8601 timestamp
body.textstring|nullPlain text body
body.htmlstring|nullHTML body
tagstring|nullCustom tag from X-LM-Tag header
headersarrayAll email headers (excluding standard ones)
attachmentsarrayFile attachments with base64 content
is_spambooleanWhether message exceeded spam threshold
spam_scorenumberCalculated spam score
spam_symbolsarrayArray of spam rule objects with name, score, options, and description
Attachment content is base64-encoded. Decode it before saving or processing files.

Processing Attachments

Attachments are included in the webhook payload with their content base64-encoded:
// Example: Save attachment in Node.js
const attachment = data.attachments[0];
const buffer = Buffer.from(attachment.content, 'base64');
fs.writeFileSync(attachment.filename, buffer);
# Example: Save attachment in Python
import base64

attachment = data['attachments'][0]
content = base64.b64decode(attachment['content'])
with open(attachment['filename'], 'wb') as f:
    f.write(content)
// Example: Save attachment in PHP
$attachment = $data['attachments'][0];
$content = base64_decode($attachment['content']);
file_put_contents($attachment['filename'], $content);

Best Practices

Security

  • Validate webhook signatures: Always verify webhook authenticity using signed webhooks
  • Sanitize email content: Email bodies can contain malicious HTML or scripts
  • Scan attachments: Run virus scans on attachments before processing
  • Rate limiting: Implement rate limits to prevent abuse

Performance

  • Async processing: Process emails in background jobs, not during webhook request
  • Return 200 quickly: Acknowledge webhook receipt within 5 seconds
  • Handle retries: Lettermint retries failed webhooks, design idempotent handlers

Use Cases

Support Ticket System

Receive support emails and automatically create tickets:
// Webhook handler
app.post('/webhooks/inbound', async (req, res) => {
  const email = req.body.data;

  // Create support ticket
  await createTicket({
    from: email.from.email,
    subject: email.subject,
    body: email.body.text,
    attachments: email.attachments,
    category: email.subaddress // Use subaddress for routing
  });

  res.sendStatus(200);
});

Email to Task Converter

Parse emails and create tasks in your project management system:
@app.route('/webhooks/inbound', methods=['POST'])
def handle_inbound():
    email = request.json['data']

    # Parse email subject for task details
    task = parse_task_from_subject(email['subject'])

    # Create task in your system
    create_task(
        title=task['title'],
        description=email['body']['text'],
        assignee=email['subaddress'],  # Use subaddress for assignment
        attachments=email['attachments']
    )

    return '', 200

Reply Tracking

Track replies to your outbound emails:
// Send email with reply tracking
$client->email
    ->from('noreply@acme.com')
    ->to('customer@example.com')
    ->replyTo('replies+order-123@acme.com')  // Unique reply address
    ->subject('Your order #123')
    ->send();

// Process reply
Route::post('/webhooks/inbound', function (Request $request) {
    $email = $request->input('data');
    $orderId = $email['subaddress']; // Extract "order-123"

    // Link reply to order
    Order::find($orderId)->addReply($email);
});

Limitations

  • Maximum email size: 25 MB (including attachments)
  • Webhook timeout: 30 seconds
  • Delivery attempts: Up to 3 retries on failure
  • Custom domains: Requires exclusive MX record pointing to Lettermint

Next Steps