How to Receive Inbound Email via Webhook

Updated 5 min read

Set up webhook-based inbound email for your AI agent with Robotomail. Register a webhook, verify HMAC signatures, parse messages, and reply in-thread.

John Joubert

John Joubert

Founder, Robotomail

How to Receive Inbound Email via Webhook
Table of contents

Setup and API examples reviewed 23 September 2026. Original publication date preserved.

Your AI agent can send email. Now it needs to hear back. This tutorial shows how to receive inbound email via webhooks using Robotomail. When someone replies to your agent, the message arrives at your endpoint as a structured JSON payload, ready to process.

Retrieve the original before replying

The webhook's data.message_id is a database ID used to retrieve or deduplicate a message. Fetch it with the GET message endpoint, then use the response's message.messageId (the RFC Message-ID) as inReplyTo. Keep data.thread_id for your application's conversation state. Do not use a database ID or thread ID as a reply header. Stop and investigate if the fetched message has no Message-ID.

The Python tool below accepts a database ID and uses this lookup before sending. Include this helper in the same module:

import os
import json
import urllib.request
from urllib.parse import quote

def get_reply_message_id(mailbox_id: str, message_id: str) -> str:
    url = (
        "https://api.robotomail.com/v1/mailboxes/"
        + quote(mailbox_id, safe="") + "/messages/" + quote(message_id, safe="")
    )
    request = urllib.request.Request(url, headers={
        "Authorization": "Bearer " + os.environ["ROBOTOMAIL_API_KEY"]
    })
    # HTTP errors propagate; do not send a reply if retrieval fails.
    with urllib.request.urlopen(request, timeout=20) as response:
        original = json.load(response)["message"]
    if not original.get("messageId"):
        raise ValueError("Original email has no Message-ID")
    return original["messageId"]

Three ways to receive email

Robotomail supports three inbound delivery methods:

  • Webhooks (push). We POST the message to your URL. Best for server-based agents with a public endpoint.
  • SSE streaming (push, no public URL). Connect to GET /v1/events and receive messages as server-sent events. Best for agents running locally or behind a firewall.
  • Polling (pull). Call GET /v1/mailboxes/:id/messages on your own schedule. Simplest to implement but adds latency.

This guide focuses on webhooks, the most common pattern for production agents.

Step 1: Register a webhook

Tell Robotomail where to send inbound messages. You can scope a webhook to a specific mailbox or receive events for all mailboxes on your account.

curl -X POST https://api.robotomail.com/v1/webhooks \
  -H "Authorization: Bearer rm_..." \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://your-app.com/webhooks/email",
    "events": ["message.received"],
    "mailboxId": "mbx_abc123"
  }'

# Response includes your webhook secret:
# {
#   "id": "wh_xyz789",
#   "secret": "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2",
#   "url": "https://your-app.com/webhooks/email",
#   "events": ["message.received"]
# }

Save the secret value. You'll use it to verify that incoming webhook payloads are genuinely from Robotomail.

Step 2: Build your endpoint

Your webhook endpoint receives a POST request with the full message payload. Here's what the payload looks like:

{
  "event": "message.received",
  "timestamp": "2026-03-26T14:30:00Z",
  "data": {
    "mailbox_id": "mbx_abc123",
    "thread_id": "thr_xyz789",
    "from": "person@example.com",
    "to": [
      "myagent@robotomail.co"
    ],
    "subject": "Re: Hello from my AI agent",
    "message_id": "msg_abc123",
    "body_text": "Sounds great, let's schedule a call.",
    "body_html": "<p>Sounds great, let's schedule a call.</p>",
    "received_at": "2026-03-26T14:30:00Z",
    "attachments": [],
    "mailbox_address": "myagent@robotomail.co",
    "cc": []
  }
}

Step 3: Verify the signature

Every webhook delivery includes an X-Robotomail-Signature header containing an HMAC-SHA256 signature. Always verify this before processing the payload.

import hmac
import hashlib

def verify_signature(payload_bytes, signature, secret):
    expected = hmac.new(
        secret.encode(),
        payload_bytes,
        hashlib.sha256,
    ).hexdigest()
    return hmac.compare_digest(expected, signature)
import { createHmac, timingSafeEqual } from "crypto";

function verifySignature(
  payloadBody: string,
  signature: string,
  secret: string,
): boolean {
  const expected = createHmac("sha256", secret)
    .update(payloadBody)
    .digest("hex");
  const actual = Buffer.from(signature);
  const digest = Buffer.from(expected);
  return actual.length === digest.length && timingSafeEqual(digest, actual);
}

Step 4: Process and reply

Once verified, parse the message and generate a reply. The thread_id field groups the conversation, and the fetched message.messageId (the RFC Message-ID) is what you pass as inReplyTo so the reply threads correctly in the recipient's email client. The same threading contract applies across every API for email your agent talks to.

@app.route("/webhooks/email", methods=["POST"])
def handle_inbound():
    # 1. Verify signature
    signature = request.headers.get("X-Robotomail-Signature", "")
    if not verify_signature(request.data, signature, WEBHOOK_SECRET):
        return "Invalid signature", 401

    # 2. Parse the message
    payload = request.json
    message = payload["data"]

    # 3. Generate reply (your LLM logic here)
    reply = generate_reply(
        from_addr=message["from"],
        subject=message["subject"],
        body=message["body_text"],
        thread_id=message["thread_id"],
    )

    # 4. Send reply, threaded to the inbound message
    requests.post(
        f"{BASE_URL}/mailboxes/{message['mailbox_id']}/messages",
        headers=headers,
        json={
            "to": [message["from"]],
            "subject": f"Re: {message['subject']}",
            "bodyText": reply,
            "inReplyTo": get_reply_message_id(message["mailbox_id"], message["message_id"]),
        },
    )

    return "", 200

Retry behavior

If your endpoint returns a non-2xx status code or times out (the delivery timeout is 10 seconds), Robotomail retries the delivery up to 5 times at 1 minute, 5 minutes, 30 minutes, 2 hours, and 12 hours. After 10 consecutive failures the webhook is automatically paused. Your endpoint should return a 200 status quickly and process the message asynchronously if needed. If you are choosing a delivery transport more broadly, webhooks vs WebSockets explains when each fits.

Next steps

You now have a complete inbound email pipeline. For related guides:

Start building, free

Give your AI agent a real email address

Create a mailbox, connect your agent and test a conversation. Send, receive and retrieve the thread through one API.

Related posts