> ## Documentation Index
> Fetch the complete documentation index at: https://docs.lettr.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Reply Tracking

> Track email replies in Lettr using variable reply-to addresses to match incoming messages to the original send and thread conversations.

Reply tracking allows you to associate incoming replies with the original emails you sent, enabling threaded conversations, support ticket systems, and automated workflows.

## How Reply Tracking Works

<Steps>
  <Step title="Send with Reply-To">
    Send an email with a unique reply-to address that encodes the conversation ID.
  </Step>

  <Step title="Recipient Replies">
    When the recipient hits reply, their email goes to your inbound domain.
  </Step>

  <Step title="Match Reply">
    Extract the conversation ID from the recipient address and match to the original email.
  </Step>

  <Step title="Thread Together">
    Add the reply to the conversation thread.
  </Step>
</Steps>

## Variable Reply-To Addresses

The most reliable way to track replies is using variable (plus) addressing in the reply-to field:

```javascript theme={null}
// Send an email with a trackable reply-to address
await lettr.emails.send({
  from: 'support@example.com',
  to: ['customer@example.com'],
  subject: 'Re: Your support request',
  html: '<p>Thanks for contacting us...</p>',
  reply_to: `reply+ticket_${ticketId}@mail.example.com`,
  reply_to_name: 'Support Team'
});
```

When the customer replies, you receive a `relay.relay_delivery` webhook event:

```json theme={null}
[
  {
    "msys": {
      "relay_message": {
        "rcpt_to": "reply+ticket_TKT-1234@mail.example.com",
        "msg_from": "customer@example.com",
        "friendly_from": "Customer Name <customer@example.com>",
        "content": {
          "subject": "Re: Your support request",
          "text": "Thanks, that fixed my issue!",
          "headers": [
            { "In-Reply-To": "<original-message-id@example.com>" },
            { "References": "<original-message-id@example.com>" }
          ]
        }
      }
    }
  }
]
```

## Processing Replies

Extract the conversation ID and match to the original:

```javascript theme={null}
// Helper to find a header value from the headers array
function getHeader(headers, name) {
  const header = headers.find(h => h[name] !== undefined);
  return header ? header[name] : null;
}

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;
    const { msg_from, content } = relay;

    // Extract ticket ID from reply address
    const match = recipient.match(/^reply\+ticket_([^@]+)@/);

    if (match) {
      const ticketId = match[1];

      // Add reply to the ticket
      await addReplyToTicket(ticketId, {
        from: msg_from,
        subject: content.subject,
        body: content.text || stripHtml(content.html),
        messageId: getHeader(content.headers, 'Message-ID'),
        inReplyTo: getHeader(content.headers, 'In-Reply-To'),
        receivedAt: new Date()
      });

      // Optionally notify the support agent
      await notifyAgent(ticketId, { from: msg_from, subject: content.subject });
    }
  }

  res.sendStatus(200);
});
```

## Using Email Headers for Threading

Standard email headers help track conversation threads:

| Header        | Purpose                                  |
| ------------- | ---------------------------------------- |
| `Message-ID`  | Unique identifier for each email         |
| `In-Reply-To` | Message-ID of the email being replied to |
| `References`  | Chain of Message-IDs in the thread       |

```javascript theme={null}
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;

    // Try to find the original message using headers
    const inReplyTo = getHeader(content.headers, 'In-Reply-To');
    const references = getHeader(content.headers, 'References');

    let thread = null;

    if (inReplyTo) {
      // Find by direct reply
      thread = await findThreadByMessageId(inReplyTo);
    }

    if (!thread && references) {
      // Find by any message in the reference chain
      const refIds = references.split(/\s+/);
      for (const refId of refIds) {
        thread = await findThreadByMessageId(refId);
        if (thread) break;
      }
    }

    if (thread) {
      await addMessageToThread(thread.id, { from: msg_from, subject: content.subject, body: content.text });
    } else {
      // Start a new thread
      await createNewThread({ from: msg_from, subject: content.subject, body: content.text });
    }
  }

  res.sendStatus(200);
});
```

## Building a Conversation Thread

Store and display threaded conversations:

```javascript theme={null}
// Data model for conversation threads
class ConversationThread {
  constructor(id) {
    this.id = id;
    this.messages = [];
    this.participants = new Set();
    this.subject = '';
    this.createdAt = null;
    this.updatedAt = null;
  }

  addMessage(message) {
    this.messages.push({
      id: message.messageId || generateId(),
      from: message.from,
      body: message.body,
      direction: message.direction, // 'inbound' or 'outbound'
      timestamp: message.timestamp || new Date()
    });

    this.participants.add(message.from);
    this.updatedAt = new Date();

    if (!this.createdAt) {
      this.createdAt = this.updatedAt;
    }
  }

  getChronological() {
    return [...this.messages].sort((a, b) =>
      new Date(a.timestamp) - new Date(b.timestamp)
    );
  }
}

// Usage
const thread = await getOrCreateThread(ticketId);

// When receiving a reply
thread.addMessage({
  from: relay.msg_from,
  body: relay.content.text,
  direction: 'inbound',
  messageId: getHeader(relay.content.headers, 'Message-ID')
});

// When sending a response
thread.addMessage({
  from: 'support@example.com',
  body: responseText,
  direction: 'outbound',
  messageId: sentEmail.messageId
});

await saveThread(thread);
```

## Auto-Reply Detection

Detect and handle auto-replies (out-of-office, delivery receipts):

```javascript theme={null}
function isAutoReply(relay) {
  const { content } = relay;
  const subject = (content.subject || '').toLowerCase();

  // Check standard auto-reply headers
  const autoSubmitted = getHeader(content.headers, 'Auto-Submitted');
  if (autoSubmitted && autoSubmitted !== 'no') {
    return true;
  }

  if (getHeader(content.headers, 'X-Auto-Response-Suppress')) {
    return true;
  }

  if (getHeader(content.headers, 'Precedence') === 'auto_reply') {
    return true;
  }

  // Check common auto-reply subject patterns
  const autoReplyPatterns = [
    /^out of office/i,
    /^automatic reply/i,
    /^auto:/i,
    /^autoreply/i,
    /^vacation/i,
    /^away from/i,
    /delivery (status )?notification/i,
    /^undeliverable:/i,
    /^returned mail/i
  ];

  for (const pattern of autoReplyPatterns) {
    if (pattern.test(subject)) {
      return true;
    }
  }

  return false;
}

app.post('/webhooks/inbound', express.json(), async (req, res) => {
  for (const event of req.body) {
    const relay = event.msys?.relay_message;
    if (!relay) continue;

    if (isAutoReply(relay)) {
      // Log but don't create a ticket or notify
      await logAutoReply(relay);
      continue;
    }

    // Process as a normal reply
    await processReply(relay);
  }

  res.sendStatus(200);
});
```

## Reply Matching Strategies

### Strategy 1: Variable Address (Recommended)

```javascript theme={null}
// Encode data in the reply-to address
const replyTo = `reply+${encodeData(conversationId)}@mail.example.com`;

function encodeData(id) {
  return Buffer.from(id).toString('base64url');
}

function decodeData(encoded) {
  return Buffer.from(encoded, 'base64url').toString();
}
```

### Strategy 2: Custom Headers

```javascript theme={null}
// Add custom headers to outgoing emails
await lettr.emails.send({
  from: 'support@example.com',
  to: ['customer@example.com'],
  subject: 'Your request',
  html: content,
  headers: {
    'X-Conversation-ID': conversationId,
    'X-Ticket-ID': ticketId
  }
});

// Note: Custom headers may not always be preserved in replies
```

### Strategy 3: Subject Line Tokens

```javascript theme={null}
// Add a token to the subject
const subject = `[Ticket #${ticketId}] Your support request`;

// Parse the token from replies
function extractTicketFromSubject(subject) {
  const match = subject.match(/\[Ticket #([^\]]+)\]/);
  return match ? match[1] : null;
}
```

### Strategy 4: Message-ID Tracking

```javascript theme={null}
// Store the Message-ID when sending
const result = await lettr.emails.send(emailData);
await storeMessageId(conversationId, result.messageId);

// Match replies using In-Reply-To header
const originalConversation = await findByMessageId(
  getHeader(relay.content.headers, 'In-Reply-To')
);
```

## Handling Reply Chains

When multiple people are involved in a thread:

```javascript theme={null}
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 { rcpt_to, msg_from, content } = relay;

    // Find the conversation
    const conversationId = extractConversationId(rcpt_to);
    const conversation = await getConversation(conversationId);

    if (!conversation) continue;

    // Add the new message
    await addMessage(conversationId, {
      from: msg_from,
      body: content.text,
      inReplyTo: getHeader(content.headers, 'In-Reply-To')
    });

    // Track all participants from To and CC headers
    const toAddresses = content.to || [];
    const ccHeader = getHeader(content.headers, 'CC');
    const allRecipients = [...toAddresses, ...(ccHeader ? [].concat(ccHeader) : [])];
    for (const recipient of allRecipients) {
      if (!recipient.includes('@mail.example.com')) {
        await addParticipant(conversationId, recipient);
      }
    }

    // Notify internal team members
    await notifyTeam(conversation, { from: msg_from, subject: content.subject, body: content.text });
  }

  res.sendStatus(200);
});
```

## Complete Support Ticket Example

```javascript theme={null}
import express from 'express';
import { v4 as uuid } from 'uuid';

const app = express();

// Send initial support response
async function sendTicketResponse(ticket, responseText) {
  const result = await lettr.emails.send({
    from: 'support@example.com',
    from_name: 'Support Team',
    to: [ticket.customerEmail],
    subject: `Re: ${ticket.subject}`,
    html: `
      <p>${responseText}</p>
      <hr>
      <p style="color: #666; font-size: 12px;">
        Ticket #${ticket.id} - Please reply above this line
      </p>
    `,
    reply_to: `reply+${ticket.id}@mail.example.com`,
    metadata: {
      ticketId: ticket.id,
      type: 'support_response'
    }
  });

  // Store the message
  await addTicketMessage(ticket.id, {
    from: 'support@example.com',
    body: responseText,
    direction: 'outbound',
    messageId: result.messageId
  });

  return result;
}

// Handle incoming replies
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 { rcpt_to, msg_from, content } = relay;

    // Skip auto-replies
    if (isAutoReply(relay)) {
      continue;
    }

    // Extract ticket ID
    const match = rcpt_to.match(/^reply\+([^@]+)@/);

    if (!match) {
      // Not a reply to a ticket - create new ticket
      const ticket = await createTicket({
        customerEmail: msg_from,
        subject: content.subject,
        body: content.text || stripHtml(content.html)
      });

      // Send acknowledgment
      await sendTicketResponse(ticket,
        'Thank you for contacting us. We\'ve received your request and will respond shortly.'
      );

      continue;
    }

    const ticketId = match[1];
    const ticket = await getTicket(ticketId);

    if (!ticket) {
      console.warn(`Ticket not found: ${ticketId}`);
      continue;
    }

    // Add customer reply to ticket
    await addTicketMessage(ticketId, {
      from: msg_from,
      body: content.text || stripHtml(content.html),
      direction: 'inbound',
      messageId: getHeader(content.headers, 'Message-ID'),
      inReplyTo: getHeader(content.headers, 'In-Reply-To')
    });

    // Update ticket status
    await updateTicket(ticketId, {
      status: 'awaiting_response',
      lastCustomerReply: new Date()
    });

    // Notify assigned agent
    if (ticket.assignedTo) {
      await notifyAgent(ticket.assignedTo, {
        ticketId,
        customerEmail: msg_from,
        preview: (content.text || '').substring(0, 100)
      });
    }
  }

  res.sendStatus(200);
});
```

## Related Topics

<CardGroup cols={2}>
  <Card title="Webhooks" icon="webhook" href="/learn/webhooks/event-types">
    Webhook payload structure
  </Card>

  <Card title="Routing" icon="route" href="/learn/inbound/routing">
    Route emails to handlers
  </Card>

  <Card title="Metadata" icon="tag" href="/learn/sending/metadata">
    Track emails with metadata
  </Card>
</CardGroup>
