Agent Email

Inbound webhooks, signed

An agent should hear about a message the moment it lands, not the next time it happens to look. Webhooks carry that moment, and the signature is what makes the payload worth acting on.

Subscribe once

Create a webhook
create_webhook {
  url: "https://your-service.dev/hooks/sending",
  events: ["email.received", "email.bounced"]
}
→ { id, secret }   # the secret is shown once

The secret is returned at creation and never again, like an API key. Deliveries are recorded, so when your endpoint was down for an hour you can see what we tried to send rather than guessing.

Verify before you trust

Every delivery carries Sending-Signature: t=,v1=. The signed string is the timestamp, a dot, and the raw body, exactly as received: re-serializing the JSON before hashing is the mistake that makes a correct implementation fail, because key order and spacing change the bytes.

Node.js
import { createHmac, timingSafeEqual } from "node:crypto";

// header: Sending-Signature: t=<unix>,v1=<hmac>
function verify(rawBody: string, header: string, secret: string): boolean {
  const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
  const expected = createHmac("sha256", secret).update(`${parts.t}.${rawBody}`).digest("hex");
  const a = Buffer.from(expected);
  const b = Buffer.from(parts.v1 ?? "");
  return a.length === b.length && timingSafeEqual(a, b);
}

Compare with a timing-safe function, and reject a timestamp older than your tolerance. An endpoint that skips both is a public inbox for anyone who learns the URL.

What to do with email.received

Treat it as a signal, not as the message. The payload tells you which inbox and which thread moved; read the thread when you are ready to act on it. That way a burst of mail does not turn into a burst of half-finished work, and a redelivery does not make your agent answer twice.

Deliveries are at-least-once by design. Make the handler idempotent on the message id, because the alternative is a duplicate reply to a customer who only wrote once.

Questions

Which events exist?

email.received, email.sent, email.delivered, email.bounced, email.complained. The first fires on inbound mail, the others follow the life of what you send: accepted by the provider, delivered, bounced, or reported as spam by the recipient.

Why verify the signature at all?

Because the URL is the only thing an attacker needs to post you a message that looks like a customer request. The signature is what separates a delivery we made from a request anyone can make. Verify it before you parse the body, and compare in a timing-safe way.

What is the timestamp for?

Replay. The signed string is the timestamp plus the raw body, so an old delivery captured on the wire cannot be resent as if it were new: reject anything older than a few minutes and the window closes.

Should I use webhooks or poll list_threads?

Webhooks. Polling costs you latency you cannot recover and calls you do not need, and it gets worse exactly when the inbox is busy. Keep a poll as a slow safety net if you like, not as the main path.

Keep reading

Start with Sending

Give your agent something worth sending.

3,000 messages a month, free. No card. The MCP endpoint is on every plan, including this one.