
A unique reply-to address gives each agent a predictable way to receive, route, and process human replies without dumping everything into one crowded inbox. The key design choice is simple: should the address identify the agent, the conversation, or both? Good systems also keep email threading intact, authenticate inbound messages, and avoid exposing internal IDs in visible headers.
For agent systems, Reply-To is more than a convenience header. It is a routing primitive. When an agent sends email, the human recipient might reply hours or weeks later, from another device, with quoted text, attachments, and a changed subject line. Your system still has to map that reply back to the right agent and state machine.
A reliable implementation needs the right address patterns, DNS and header mechanics, routing tables, webhook payloads, and failure handling when giving each agent its own reply path.
What a unique Reply-To address should solve
A unique Reply-To address should help your inbound pipeline answer three questions fast: which agent owns this reply, which conversation or task it belongs to, and whether the message is trusted enough to enter the agent workflow. If the address answers only one of those questions, you need more routing signals from headers, message IDs, or your database.
A basic outbound message has at least three sender-related fields:
From: the visible author shown by most mail clients.Reply-To: where mail clients should send replies.Return-Path: where bounces go, usually set by the sending infrastructure.
For agent routing, Reply-To usually matters most because it controls human replies. A recipient can see it if they inspect message details, but most people just click reply and let their mail client handle it.
Example:
From: Support Agent <support@company.example>
Reply-To: Alex Agent <reply+agent_7f3a@agents.company.example>
Message-ID: <msg_01JZ8P9K4A@agents.company.example>
Subject: Re: Billing question
When the user replies, your inbound service receives mail at reply+agent_7f3a@agents.company.example. That address becomes the routing entry point.
The design should handle these requirements:
- Agent identity: map the reply to the agent instance or persona.
- Conversation identity: map the reply to a thread, ticket, lead, workflow run, or memory record.
- Threading: preserve
Message-ID,In-Reply-To, andReferencesso clients group messages correctly. - Security: reject or quarantine spoofed, malformed, or unauthorized inbound mail.
- Privacy: avoid exposing sequential database IDs or sensitive tenant structure.
- Operations: support rotation, deactivation, forwarding, logging, and replay.
If you already use an email API for agents, treat reply addresses as part of your application data model, not as a last-minute option in a send call.
Addressing patterns for agent Reply-To routing
The three common patterns are plus addressing, generated aliases, and opaque per-thread addresses. Plus addressing is simple and works well for small systems. Generated aliases work better for long-lived agent identities. Opaque per-thread addresses give the most precise routing and better privacy at higher scale.
Here is the practical comparison:
| Pattern | Example | Best for | Main weakness |
|---|---|---|---|
| Plus addressing | reply+agent_123@agents.example.com |
Fast implementation, internal tools | Some systems normalize or expose the tag |
| Per-agent alias | alex@agents.example.com |
Human-readable agent identities | Needs extra signal for conversation routing |
| Opaque token | r.8k4p2x9q@agents.example.com |
Privacy, per-thread routing | Requires token store and lifecycle controls |
| Hybrid token | a7.t9k2@agents.example.com |
Agent plus thread routing | More complex parsing and rotation |
Pattern 1: plus addressing
Plus addressing uses the local part after + as a tag:
reply+agent_7f3a@agents.company.example
reply+agent_7f3a.thread_91c2@agents.company.example
Your inbound router splits the local part:
function parsePlusAddress(address: string) {
const [local, domain] = address.toLowerCase().split('@');
const [mailbox, tag] = local.split('+');
if (mailbox !== 'reply') throw new Error('unexpected mailbox');
if (!tag) throw new Error('missing routing tag');
return { mailbox, tag, domain };
}
This is easy to build, but do not assume every downstream system keeps plus tags forever. Most modern mail systems do, but CRMs, forwarding rules, help desks, and user-entered contact records sometimes strip or rewrite them. Plus addressing works when you control most of the path. It gets weaker when replies pass through many systems.
Pattern 2: per-agent alias
A per-agent alias gives each agent a stable visible address:
sales-alex@agents.company.example
renewals-bot@agents.company.example
vendor-intake@agents.company.example
This helps when humans should recognize the agent across conversations. The tradeoff: sales-alex@... tells you only the agent. To identify the specific conversation, you still need one or more of:
In-Reply-Tomatching an outboundMessage-ID.Referencescontaining a known prior message ID.- A hidden token in the address, headers, or body.
- A recent open task for that sender and agent.
Per-agent aliases work best when one agent owns a limited number of active conversations per sender. They become ambiguous when the same contact can discuss multiple deals, tickets, or workflows with the same agent.
Pattern 3: opaque per-thread addresses
Opaque addresses use generated tokens with no obvious meaning:
r.j8f4q2ma9x@agents.company.example
r.n6p1v0cz3k@agents.company.example
In your database, each token maps to a route:
create table inbound_reply_routes (
token text primary key,
tenant_id uuid not null,
agent_id uuid not null,
conversation_id uuid not null,
outbound_message_id text not null,
status text not null default 'active',
expires_at timestamptz,
created_at timestamptz not null default now()
);
This is the safest pattern for production agent workflows because it avoids exposing internal IDs and routes replies without relying on subject lines. Generate at least 96 bits of randomness, encode it in base32 or base64url, and store only the token hash if you want defense in depth.
Example token generation:
import crypto from 'node:crypto';
function createReplyToken(bytes = 16) {
return crypto.randomBytes(bytes).toString('base64url');
}
const token = createReplyToken();
const replyTo = `r.${token}@agents.company.example`;
Sixteen random bytes gives 128 bits of entropy. That is enough to prevent guessing. Do not use sequential tokens like thread_100042.
How Reply-To interacts with From, DKIM, SPF, and DMARC
Reply-To controls where replies go, but it does not authenticate the sender. SPF checks the envelope sender domain, DKIM signs selected headers, and DMARC evaluates alignment against the visible From domain. Your unique Reply-To domain should be configured to receive mail, while your From domain still has to pass outbound authentication.
A common setup uses separate subdomains:
From: Agent <agent@company.example>
Reply-To: Agent <r.q8m2...@reply.company.example>
Return-Path: bounce@bounce.company.example
Each domain has a different role:
company.example: visible brand domain inFrom.reply.company.example: inbound MX target for replies.bounce.company.example: envelope sender domain for bounces.
For outbound authentication:
- SPF: authorizes the sending service for the envelope sender domain.
- DKIM: signs headers such as
From,To,Subject,Date, and oftenMessage-ID. - DMARC: checks that SPF or DKIM aligns with the visible
Fromdomain.
Reply-To does not need DMARC alignment because DMARC does not evaluate it as the identity. Still, you should control the domain and publish MX records so replies are accepted reliably.
A minimal DNS shape looks like this:
company.example. TXT "v=spf1 include:send-provider.example -all"
_dmarc.company.example. TXT "v=DMARC1; p=quarantine; rua=mailto:dmarc@company.example"
selector._domainkey.company.example. TXT "v=DKIM1; k=rsa; p=..."
reply.company.example. MX 10 inbound.mail-provider.example.
If you use Mails.ai as the email infrastructure for agent send and receive flows, the platform can sit behind these domains while your app still owns the routing model. Keep the application-level token mapping in your database even when a provider handles parsing and webhooks.
Routing model: agent ID, conversation ID, or both
A Reply-To address can encode the agent, the conversation, or a route token that maps to both. For most agent systems, the best default is a per-conversation token tied to an agent ID in your database. It gives precise routing and still lets you rotate agents or reassign ownership later.
Think of routing as a lookup, not string parsing alone. The inbound address is just the key.
A solid route record should include:
tenant_id: prevents cross-tenant confusion.agent_id: the current owner of the reply.conversation_id: the state object to resume.outbound_message_id: the message that created the route.allowed_senders: optional list of expected reply senders.status: active, expired, disabled, archived.expires_at: optional expiry for short-lived workflows.created_by_run_id: useful for replay and debugging.
Example route creation during send:
type ReplyRoute = {
token: string;
tenantId: string;
agentId: string;
conversationId: string;
outboundMessageId: string;
allowedSenders: string[];
};
async function createRoute(input: Omit<ReplyRoute, 'token'>) {
const token = crypto.randomBytes(16).toString('base64url');
await db.inboundReplyRoutes.insert({
tokenHash: sha256(token),
tenantId: input.tenantId,
agentId: input.agentId,
conversationId: input.conversationId,
outboundMessageId: input.outboundMessageId,
allowedSenders: input.allowedSenders,
status: 'active'
});
return `r.${token}@reply.company.example`;
}
On inbound, reverse the process:
async function routeInbound(recipient: string, from: string) {
const token = extractToken(recipient); // r.TOKEN@reply.company.example
const route = await db.inboundReplyRoutes.findByTokenHash(sha256(token));
if (!route || route.status !== 'active') {
return { action: 'quarantine', reason: 'unknown_or_disabled_route' };
}
if (route.allowedSenders.length > 0 && !route.allowedSenders.includes(normalizeEmail(from))) {
return { action: 'review', reason: 'unexpected_sender', route };
}
return { action: 'deliver', route };
}
Do not let the agent decide where the message belongs after reading the body. Route first, then reason. LLM classification can help decide the next action, but it should not be your primary database key.
Preserve threading with Message-ID, In-Reply-To, and References
Unique Reply-To addresses route messages to your backend, but email clients thread conversations using Message-ID, In-Reply-To, References, subject normalization, and provider-specific heuristics. If you ignore these headers, replies might route correctly in your system but look broken in the user's inbox.
Every outbound agent email should store its provider message ID and its RFC message ID. They are not always the same.
Example outbound headers:
Message-ID: <agentmsg_01JZ8QVWJ2H8Y7ZP@company.example>
Subject: Contract review for Acme
Reply-To: Review Agent <r.dBj7...@reply.company.example>
A human reply will often include:
In-Reply-To: <agentmsg_01JZ8QVWJ2H8Y7ZP@company.example>
References: <agentmsg_01JZ8QVWJ2H8Y7ZP@company.example>
When your agent responds again, include the correct reply headers:
Message-ID: <agentmsg_01JZ9A3X7DRT0V9K@company.example>
In-Reply-To: <humanmsg_abc123@mail.example>
References: <agentmsg_01JZ8QVWJ2H8Y7ZP@company.example> <humanmsg_abc123@mail.example>
Routing should prefer the unique recipient address, then use threading headers as a consistency check:
- Parse the inbound recipient and find the route token.
- Parse
In-Reply-ToandReferences. - Confirm at least one header maps to the same conversation when possible.
- If address and headers disagree, quarantine or review.
- Store the inbound message with raw headers for audit and replay.
This prevents bugs where forwarded mail, copied aliases, or old reply addresses push a message into the wrong agent memory.
End-to-end flow for unique Reply-To routing
The send path creates a route before email leaves your system. The receive path resolves that route before the agent reads the message. That separation keeps routing deterministic and makes retries safe. The agent should receive a normalized event that already contains agent_id, conversation_id, and parsed message content.
A typical flow looks like this:
sequenceDiagram participant App participant DB participant Mail participant User participant Agent App->>DB: CreateRoute App->>Mail: SendWithReplyTo Mail->>User: DeliverEmail User->>Mail: Reply Mail->>App: InboundWebhook App->>DB: ResolveRoute App->>Agent: DeliverEvent
The main rule is simple: create the route before sending. If route creation succeeds but send fails, mark the route inactive or leave it unused with an expiry. If send succeeds but route creation failed, replies may be unroutable.
A safer send transaction uses an outbox table:
create table agent_email_outbox (
id uuid primary key,
tenant_id uuid not null,
agent_id uuid not null,
conversation_id uuid not null,
to_email text not null,
subject text not null,
body_html text not null,
reply_to text not null,
status text not null default 'pending',
provider_message_id text,
created_at timestamptz not null default now()
);
Then process it:
async function sendPendingEmail(row: OutboxRow) {
const result = await mail.send({
from: 'Agent <agent@company.example>',
to: row.toEmail,
replyTo: row.replyTo,
subject: row.subject,
html: row.bodyHtml,
headers: {
'X-Agent-ID': row.agentId,
'X-Conversation-ID': row.conversationId
}
});
await db.agentEmailOutbox.update(row.id, {
status: 'sent',
providerMessageId: result.id
});
}
Custom X-* headers help with internal observability, but do not rely on them for replies. Many clients preserve them poorly, and forwarded messages often drop them. Use the recipient address and standard threading headers instead.
For receive, a webhook payload should give you enough data to route and process without fetching raw MIME unless needed:
{
"event_id": "evt_01JZ9Q2X8G",
"recipient": "r.dBj7M9k...@reply.company.example",
"from": "pat@example.com",
"subject": "Re: Contract review for Acme",
"message_id": "<humanmsg_abc123@mail.example>",
"in_reply_to": "<agentmsg_01JZ8QVWJ2H8Y7ZP@company.example>",
"references": ["<agentmsg_01JZ8QVWJ2H8Y7ZP@company.example>"],
"text": "Looks good. Please send the final copy.",
"attachments": []
}
If your provider supports inbound email parsing, use parsed fields for routing but still store raw MIME or raw headers when the message affects money, contracts, access, or compliance.
Security checks before the agent reads the reply
A unique Reply-To address is a routing key, not proof of authority. Anyone who sees or guesses the address can send to it. Before injecting an inbound message into an agent, verify the route, sender, authentication results, attachment safety, and replay status.
At minimum, perform these checks:
1. Verify the route token
Unknown tokens should not hit the agent. Return a generic SMTP response if you operate the MX directly, or accept and quarantine if your provider already accepted the message. Avoid detailed bounce text that reveals whether a token exists.
2. Check the sender against expected participants
If the outbound email went to pat@example.com, a reply from pat@example.com is expected. A reply from assistant@pat-company.example may also be valid. A reply from an unrelated domain should go to review.
Use normalized addresses:
function normalizeEmail(email: string) {
return email.trim().toLowerCase();
}
Be careful with Gmail-style local-part normalization. Do not remove dots or plus tags globally; those rules are provider-specific.
3. Read authentication results
Inbound authentication can help classify risk. Look for parsed Authentication-Results values such as:
spf=pass smtp.mailfrom=example.com;
dkim=pass header.d=example.com;
dmarc=pass header.from=example.com
A DMARC failure does not always mean the content is malicious, especially with forwarded mail. But it should lower trust for high-impact actions.
4. Enforce idempotency
Inbound providers may retry webhooks. Mail servers may redeliver. Humans may send duplicates. Use a stable key such as provider event ID plus message ID plus recipient.
create unique index inbound_unique_message
on inbound_messages(provider_event_id);
If the provider event ID is not stable across retries, use a composite key:
create unique index inbound_unique_fallback
on inbound_messages(message_id, recipient, from_email);
5. Scan attachments before LLM ingestion
Attachments can be huge, malformed, password-protected, or malicious. Do not pass raw files directly into an agent tool. Store them, scan them, extract text with size limits, and pass a safe representation.
For workflows that need automatic decisions, combine deterministic routing with email classification. Classification should decide intent, urgency, or required action. It should not replace authentication and route validation.
Privacy and lifecycle management
Reply addresses live longer than you expect because people forward old messages, archive threads, and reply months later. Use opaque tokens, expiration policies, and disabled route states so you can control what happens when an agent is deleted, reassigned, or compromised.
A good route lifecycle has these states:
| State | Meaning | Inbound behavior |
|---|---|---|
active |
Normal route | Deliver to agent workflow |
paused |
Temporary stop | Store and notify operator |
expired |
Time window closed | Send polite auto-response or archive |
disabled |
Security or deletion | Quarantine silently |
merged |
Conversation moved | Redirect to new conversation |
Avoid putting sensitive information in the visible address:
Bad:
reply+tenant_acme.user_pat.contract_renewal_900k@reply.example.com
Better:
r.b7wKp9qJ2sLm4xQv@reply.example.com
If humans need recognizable addresses, use display names rather than meaningful local parts:
Reply-To: Acme Renewal Agent <r.b7wKp9qJ2sLm4xQv@reply.example.com>
Display names are not secure either, but they improve usability without turning the address into a data leak.
For long-running agent systems, plan for rotation:
- Generate a new reply token for each new outbound thread.
- Keep old tokens active while the thread is active.
- Expire tokens after a defined inactivity period for low-risk workflows.
- Keep high-value workflows active but guarded by sender checks and review queues.
- Record token creation and resolution events for audit.
Implementation options: build on SMTP or use an agent email API
You can implement unique Reply-To addresses directly with MX records, SMTP receiving, MIME parsing, and webhooks. You can also use an agent email platform that exposes inbound events and routing metadata. The right choice depends on how much mail infrastructure you want to own.
If you build it yourself, you need:
- A domain or subdomain for replies.
- MX records pointed at your receiving service.
- SMTP server or inbound provider integration.
- MIME parser that handles text, HTML, attachments, encodings, and malformed messages.
- Webhook or queue delivery into your agent runtime.
- Storage for raw messages and parsed bodies.
- Abuse handling, rate limits, suppression, and monitoring.
If you use a provider, inspect these capabilities before choosing:
| Capability | Why it matters for agents |
|---|---|
| Inbound parsing | Agents need clean text, HTML, headers, and attachments |
| Stable webhooks | Retries and idempotency prevent lost replies |
| Custom Reply-To | You need per-agent or per-thread routing |
| Raw MIME access | Debugging and compliance often require originals |
| Thread header support | Agents must preserve client-side conversation views |
| Deliverability controls | Automated senders need reputation management |
| MCP support | Agents can use email as a tool without custom glue |
Mails.ai provides MCP-native email and inbound parsing for agent workflows, which can reduce the infrastructure you need to maintain. Even then, keep your route table explicit. Your app owns the meaning of agent_id, conversation_id, and what an inbound reply is allowed to trigger.
Common mistakes that break agent reply routing
Most failures come from treating email like a stateless API call. Email is asynchronous, mutable, and full of intermediaries. Build for delayed replies, changed subjects, forwarded messages, duplicate deliveries, and partial authentication instead of assuming a clean request-response loop.
Watch for these mistakes:
Using one shared Reply-To address for every agent
A shared address like replies@company.example forces you to infer routing from subject lines or message bodies. That works until two active conversations share a subject, a user deletes quoted text, or a forward changes headers.
Encoding raw database IDs
reply+agent_42_thread_991@... is easy to debug but leaks structure. Use opaque tokens in production. Log the mapping internally.
Relying only on the subject line
Subjects are user-editable. Clients add prefixes like Re:, Fwd:, localized variants, ticket tags, and mobile signatures. Use subjects for display and fallback search, not primary routing.
Ignoring forwarded replies
Forwarded messages may arrive from a new sender with the old thread content embedded in the body. Decide how to handle them:
- If the route token is preserved, route but mark as forwarded or unexpected sender.
- If only old headers are present, review before agent action.
- If no route signal exists, classify into a general intake queue.
Letting agents execute high-impact actions from untrusted replies
A reply that says "approve it" should not automatically move money, grant access, or sign a contract unless the sender and route are trusted and the action policy allows it. Require confirmation for high-risk intents.
Not storing raw inbound data
Parsed text is not enough for debugging. Store raw headers at minimum. For regulated workflows, store raw MIME with retention controls.
Recommended default architecture
For most teams, the best default is an opaque per-conversation Reply-To address on a dedicated reply subdomain, backed by a route table and inbound webhook processing. Use standard threading headers for client behavior, route tokens for backend mapping, and sender/authentication checks for safety.
A production-ready default looks like this:
- Use
reply.company.examplefor inbound replies. - Generate a random token for each outbound conversation.
- Store
hash(token) -> tenant_id, agent_id, conversation_id. - Send with
Reply-To: Display Name <r.token@reply.company.example>. - Store outbound
Message-IDand provider ID. - Receive inbound mail through webhook or SMTP.
- Resolve route by recipient token.
- Cross-check
In-Reply-ToandReferenceswhen present. - Validate sender and authentication results.
- Deliver a normalized event to the agent runtime.
- Store raw headers and parsed body.
- Apply classification and action policy.
This model scales from a few agents to millions of routes because the address is just a lookup key. It also keeps the agent runtime clean: by the time the agent sees the message, routing, parsing, deduplication, and basic trust checks have already happened.
If you want to understand where this fits in a broader system, the architecture view is useful: Reply-To routing sits between outbound send orchestration and inbound event delivery, not inside the model prompt.
Frequently Asked Questions
Should each agent get a unique email address or just a unique Reply-To address?
Use both when the agent needs a stable public identity. Use only unique Reply-To addresses when the visible sender can stay shared, such as support@company.example, but replies must route to different agents or conversations. The From address affects trust and branding; Reply-To affects routing.
Is plus addressing safe for production agent replies?
Plus addressing can work, especially for internal tools and controlled environments. For production customer workflows, opaque per-thread addresses are safer because they do not expose IDs and depend less on downstream systems preserving tags exactly.
Can I use the same Reply-To address for all messages in a conversation?
Yes. That is usually the right design. A conversation-level Reply-To token lets every reply in that thread map to the same state object. If the conversation changes ownership, update the route record instead of changing old emails.
Do SPF, DKIM, and DMARC apply to Reply-To?
Not directly in the way they apply to the visible From domain. DMARC evaluates alignment against From. Still, your Reply-To domain must have working MX records, and inbound authentication results should influence how much your agent trusts a reply.
What should happen when a reply arrives for an expired token?
Do not drop it silently unless the route was disabled for abuse. For normal expiry, store the message and either send a polite auto-response or route it to a review queue. People often reply to old email threads after long delays.
Should an LLM classify replies before routing?
No. Route deterministically first using the recipient address and headers. After routing, use classification to decide intent, priority, sentiment, or next action. This keeps database identity separate from language understanding.