Home / Blog / Email webhooks: how inbound mail triggers your agent
Deep divePolling 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.
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.{
"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.
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).
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.
Working end-to-end consumer (verification, decide-or-escalate, reply): mektup-inbox-agent on GitHub.
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.
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.