
how to handle ai agent inbound email webhook processing
Learn how AI agents process inbound emails through webhooks, from payload parsing to async execution, with code examples and security best practices.
Your agent gets an email. A customer replied, a form submitted, or an OTP just landed. What happens next depends entirely on how you've wired up your inbound email webhook processing. Get it right and your agent reacts in seconds. Get it wrong and emails pile up unread, duplicates trigger double actions, or worse, a spoofed payload tricks your agent into doing something you didn't authorize.
Most guides on AI agent inbound email webhook processing cover the happy path: receive POST, parse JSON, do thing. That's table stakes. The real work is in verification, idempotency, async execution, and failure recovery. This is the stuff that separates a demo from a production system.
How to process inbound email webhooks for AI agents#
AI agent inbound email webhook processing is the pattern where your email provider sends an HTTP POST to your agent's endpoint every time a new message arrives, replacing the need for polling. Here's the full sequence:
- Provision an API inbox so your agent has its own email address.
- Register a webhook URL pointing to your agent's HTTP endpoint.
- Receive the POST payload when an inbound email arrives.
- Return a 200 OK response immediately, before doing any processing.
- Verify the webhook signature using HMAC to confirm authenticity.
- Parse the payload fields (sender, subject, body, thread ID, attachments).
- Run your agent logic asynchronously and send a reply via API.
Each of these steps has failure modes worth understanding. Let's walk through the ones that trip people up.
Return 200 first, process second#
This is the single most common mistake in webhook handler design. Your agent receives the POST, starts parsing the email, builds a prompt, calls an LLM, generates a reply, sends it, and then returns 200. The whole round trip takes 8 seconds. Your email provider waited 5 seconds, got no response, and queued a retry.
Now your agent processes the same email twice. If it's a payment confirmation, your agent might credit the account twice. If it's a support request, the customer gets two identical replies.
The fix is simple in concept: acknowledge the webhook instantly and process everything in a background job.
app.post('/webhook/email', async (req, res) => {
// Acknowledge immediately
res.status(200).send('ok');
// Process asynchronously
setImmediate(async () => {
const verified = verifySignature(req);
if (!verified) return;
const { emailId, from, subject, preview, threadId } = req.body.data;
await agentProcessEmail({ emailId, from, subject, preview, threadId });
});
});
On serverless platforms like AWS Lambda or Cloudflare Workers, you can't easily run logic after returning a response. In those environments, push the payload to a queue (SQS, Kafka, or even a simple Redis list) and have a separate worker consume it.
Verify webhook signatures#
Every webhook payload should be signed by your email provider. If you skip verification, anyone who discovers your endpoint URL can send fake payloads. Imagine a spoofed email.received event with a from field set to your CEO's address and a body that says "approve the wire transfer." Your agent would process it as a legitimate instruction.
LobsterMail signs every webhook with an HMAC-SHA256 signature using the secret returned at creation time. Verification looks like this:
import crypto from 'crypto';
function verifySignature(req: Request, secret: string): boolean {
const signature = req.headers['x-lobstermail-signature'];
const expected = crypto
.createHmac('sha256', secret)
.update(JSON.stringify(req.body))
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expected)
);
}
Use timingSafeEqual instead of === to prevent timing attacks. Store the webhook secret in environment variables, not in code.
Handle idempotency#
Webhook providers retry on failure. Sometimes they retry even on success (network hiccup, ambiguous timeout). Your handler needs to be safe to call multiple times with the same payload.
The simplest approach: store every processed emailId in a set (Redis, a database table, even an in-memory Map for prototypes) and skip duplicates at the top of your handler.
async function agentProcessEmail(data: EmailPayload) {
const already = await redis.sismember('processed_emails', data.emailId);
if (already) return;
await redis.sadd('processed_emails', data.emailId);
// Your actual agent logic here
const response = await generateAgentReply(data);
await sendReply(data.threadId, response);
}
Set a TTL on your dedup keys (72 hours is usually plenty) so the set doesn't grow forever.
Route emails to the right agent logic#
Not every inbound email should trigger the same behavior. A password reset OTP needs extraction and submission. A customer question needs a thoughtful reply. A newsletter should be ignored entirely.
Route based on recipient address, subject patterns, or sender domain:
function routeEmail(data: EmailPayload) {
const { to, subject, from } = data;
if (to.includes('support@')) return handleSupportRequest(data);
if (subject.match(/OTP|verification|code/i)) return extractOTP(data);
if (from.endsWith('.noreply.com')) return archive(data);
return handleGeneral(data);
}
With LobsterMail, each agent can have its own dedicated inbox and webhook URL, which means routing happens at the infrastructure level rather than inside your handler code. One inbox for support, another for notifications, a third for outbound campaign replies. Each points to a different endpoint.
Threading and conversation context#
When your agent replies to an email, the reply needs to land in the same thread. This isn't just a UX nicety. If your agent starts a new thread for every response, the human on the other end sees disconnected messages with no context, and their replies back won't correlate to your agent's conversation state.
LobsterMail's webhook payload includes a threadId field. Store it alongside your conversation history and use it when sending replies:
const reply = await lm.sendEmail({
inboxId: 'ibx_xyz789',
threadId: data.threadId,
to: [data.from],
subject: `Re: ${data.subject}`,
body: agentResponse,
});
This keeps the entire conversation threaded on both sides.
Testing webhooks locally#
You can't receive webhooks on localhost. During development, use a tunneling tool to expose your local server:
npx cloudflared tunnel --url http://localhost:3000
This gives you a public HTTPS URL you can register as your webhook endpoint. Send a test email to your agent's inbox and watch the payload hit your local handler. Once you've confirmed the flow works, swap the URL for your production endpoint.
For CI environments, save a few real webhook payloads as JSON fixtures and replay them against your handler in tests. This catches parsing regressions without needing a live email provider in the loop.
What about polling?#
IMAP polling still works. It's simpler to set up and doesn't require a public endpoint. But it comes with real tradeoffs: you're either polling too often (wasting resources, hitting rate limits) or too infrequently (your agent takes minutes to respond instead of seconds). For agents that need to react to inbound email in near-real-time, webhooks are the right choice.
If your agent checks email once an hour to summarize a daily digest, polling is fine. If it's handling support tickets, processing inbound leads, or extracting OTPs for multi-step workflows, you want webhooks.
Where to go from here#
If your agent doesn't have its own inbox yet, that's the first step. , paste the instructions to your agent, and it handles provisioning and webhook registration itself. From there, wire up your handler with the patterns above: instant 200, signature verification, idempotency, and async processing. That's the whole stack.
Frequently asked questions
What does inbound email webhook processing mean for an AI agent?
It means your email provider sends an HTTP POST request to your agent's endpoint whenever a new email arrives. Your agent parses the payload, decides what to do, and acts on it, all without polling or manual checks.
Why should I use webhooks instead of IMAP polling for my AI agent's email?
Webhooks deliver emails to your agent in near-real-time (typically under 2 seconds) with no wasted API calls. IMAP polling introduces latency, burns rate limits, and requires managing connection state.
How do I immediately return a 200 OK and process the email asynchronously?
Send res.status(200).send('ok') before running any logic. Then use setImmediate, a background queue, or a worker function to handle parsing and agent execution after the response is sent.
How do I verify that a webhook request actually came from my email provider?
Check the HMAC-SHA256 signature in the request headers against the webhook secret you stored at creation time. Use crypto.timingSafeEqual for the comparison to prevent timing attacks.
What is idempotency and why does it matter for email webhook handlers?
Idempotency means processing the same webhook payload multiple times produces the same result as processing it once. It matters because providers retry deliveries on timeouts, and without deduplication your agent may act on the same email twice.
How do I deduplicate emails when a webhook fires more than once for the same message?
Store each processed emailId in Redis or a database. At the start of your handler, check if the ID already exists. If it does, skip processing. Set a TTL of 48-72 hours on dedup keys to keep storage bounded.
Can multiple AI agents each have their own dedicated inbox and webhook URL?
Yes. With LobsterMail, each agent can provision its own inbox and register a separate webhook URL. This means routing happens at the infrastructure level rather than inside your application code.
How do I handle email threading so an agent's reply stays in the same conversation?
Use the threadId from the webhook payload when sending replies. This keeps your agent's responses threaded with the original message on both the sender's and recipient's email clients.
What happens if my webhook endpoint is temporarily down? Will emails be lost?
Most providers (including LobsterMail) retry failed deliveries with exponential backoff over several hours. Emails are not lost, but your handler must be idempotent to safely process retried payloads.
How do I extract OTPs or magic links from an inbound webhook payload?
Parse the email body (plain text or HTML) with a regex pattern matching 4-8 digit codes or URLs containing tokens. LobsterMail's payload includes a preview field with the first 200 characters, which often contains the OTP directly.
Which serverless platforms work best for email webhook handlers?
Cloudflare Workers and Vercel Edge Functions have the lowest cold-start latency. AWS Lambda works well when paired with SQS for async processing. The key constraint is returning 200 before your function times out.
Can I use n8n or Zapier to connect inbound email webhooks to an AI agent?
Yes. Both support incoming webhooks as triggers. Point your email provider's webhook URL at your n8n or Zapier endpoint, then wire the parsed fields into your agent's prompt or API call within the workflow builder.
How do I test inbound email webhooks during local development?
Use a tunneling tool like Cloudflare Tunnel or ngrok to expose your local server with a public HTTPS URL. Register that URL as your webhook endpoint, send a test email, and watch payloads arrive on your local machine.
What retry policies should I expect from email webhook providers?
Most providers retry 3-5 times over a window of 1-24 hours using exponential backoff. LobsterMail retries failed deliveries automatically. Design your handler to be idempotent regardless of the specific retry policy.
How do I route inbound emails to different agent logic based on recipient address?
Either use separate inboxes with separate webhook URLs (one per function), or inspect the to field in the payload and route within your handler. Separate inboxes are cleaner at scale.


