Home / Blog / Email webhooks: how inbound mail triggers your agent

Deep dive

Email webhooks: how inbound mail triggers your agent

Polling an inbox every 30 seconds is how email automation used to work. A webhook flips the model: the moment a message lands in the mailbox, your endpoint gets a signed POST with the sender, subject, and body already in it. Here's exactly how Mektup's mailbox webhooks work and how to consume them safely.

The model: one URL per mailbox

Every Mektup mailbox can have one webhook URL. On every new inbound message, Mektup POSTs to it immediately - typically within seconds of the mail hitting the MX. Setting it is one call (or the set_mailbox_webhook MCP tool):

curl -X PUT "https://api.usemektup.com/v1/domains/yourdomain.com/mailboxes/agent/webhook" \
  -H "Authorization: Bearer mek_live_..." \
  -H "Content-Type: application/json" \
  -d '{"url": "https://your-app.example/mektup-webhook"}'

# response includes "secret" - shown ONCE. Store it; every delivery
# is HMAC-signed with it.

The payload

{
  "event": "message.received",
  "mailbox": "agent@yourdomain.com",
  "message": {
    "id": "uuid",
    "from": "sender@example.org",
    "to": "agent@yourdomain.com",
    "subject": "...",
    "text": "plain-text body, capped at 20000 chars",
    "receivedAt": "...",
    "threadKey": "..."
  }
}

Sender, subject, and body arrive in the payload - for most agent decisions you never need a second API call. Need the full message or attachments? Fetch by id via GET /v1/messages/:id.

Verify the signature - always

Anyone on the internet can POST to your endpoint. Every genuine delivery carries X-Mektup-Signature: sha256=<hex> - an HMAC-SHA256 of the exact raw request body with your secret. Verify before trusting a byte:

// Express: use express.raw() on this route - you need the RAW body,
// a JSON-parsed-then-restringified body will NOT match the signature
const sig = (req.get("X-Mektup-Signature") ?? "").replace(/^sha256=/, "");
const expected = crypto.createHmac("sha256", SECRET)
                       .update(req.body).digest("hex");
const a = Buffer.from(expected), b = Buffer.from(sig);
if (a.length !== b.length || !crypto.timingSafeEqual(a, b))
  return res.status(401).end();

Two classic mistakes: comparing with === (timing side channel - use timingSafeEqual) and verifying against a re-serialized body (breaks byte-for-byte matching - keep the raw buffer).

Ack fast, process after

Mektup's delivery is deliberately simple: single attempt, 8-second timeout, no retry queue. A failed delivery is logged server-side and never affects the mail itself - the message is already durably stored in the inbox either way. So: respond 200 immediately, then do the slow work (model calls, DB writes). If your pipeline needs guaranteed processing, run a periodic reconciliation poll of GET /v1/messages to catch anything a downed endpoint missed - the webhook is the fast path, not the source of truth.

Why this design is right for agents

  • Latency: an agent that replies in seconds feels like a colleague; one that polls every minute feels like a cron job.
  • Cost: no wasted list calls on an idle mailbox - and receiving is unmetered on Mektup anyway.
  • Simplicity: because the mailbox is the durable store, your webhook consumer can be stateless and crash-safe. The inbox IS the queue, with a human-readable UI for free.
  • Per-tenant fan-out: one URL per mailbox means each customer's or agent's mailbox can hit its own endpoint with its own secret - the natural shape for per-customer inboxes.

Working end-to-end consumer (verification, decide-or-escalate, reply): mektup-inbox-agent on GitHub.

Get started free

FAQ

Common questions

How does an AI agent get notified of new email without polling?

With a per-mailbox webhook. Mektup POSTs to your URL the instant a new inbound message lands, with sender, subject, and plain-text body already in the payload, signed with HMAC-SHA256 so you can verify authenticity. The agent reacts in seconds; no list-messages polling loop.

What happens if my webhook endpoint is down when an email arrives?

Nothing is lost. The message is already durably stored in the mailbox before the webhook fires; delivery is single-attempt with an 8-second timeout and failures are logged, never retried. Endpoints that need guaranteed processing should also run a periodic reconciliation poll of GET /v1/messages.

Ready when you are

Event-driven email, no polling

Get started free