CtrlK
BlogDocsLog inGet started
Tessl Logo

markusdowne/agentmail

Give AI agents their own email inboxes using the AgentMail API. Use when building email agents, sending/receiving emails programmatically, managing inboxes, handling attachments, organizing with labels, creating drafts for human approval, or setting up real-time notifications via webhooks/websockets. Supports multi-tenant isolation with pods.

100

1.20x
Quality

100%

Does it follow best practices?

Impact

100%

1.20x

Average score across 1 eval scenario

SecuritybySnyk

Advisory

Suggest reviewing before use

Overview
Quality
Evals
Security
Files

webhooks.mdreferences/

Webhooks

Webhooks provide real-time HTTP notifications when email events occur. Use webhooks when you have a public URL endpoint.

When to Use

  • Production applications with public endpoints
  • Event-driven architectures
  • When you need to process events on your server

For local development without a public URL, use websockets.md instead.

Setup

Register a webhook endpoint to receive events.

import { AgentMailClient } from "agentmail";

const apiKey = process.env.AGENTMAIL_API_KEY;
if (!apiKey) throw new Error("Set AGENTMAIL_API_KEY in the environment");
const client = new AgentMailClient({ apiKey });

// Create webhook
const webhook = await client.webhooks.create({
  url: "https://your-server.com/webhooks",
});

// List webhooks
const webhooks = await client.webhooks.list();

// Delete webhook
await client.webhooks.delete({ webhookId: webhook.webhookId });
import os
from agentmail import AgentMail

api_key = os.environ.get("AGENTMAIL_API_KEY")
if not api_key:
    raise ValueError("Set AGENTMAIL_API_KEY in the environment")
client = AgentMail(api_key=api_key)

# Create webhook
webhook = client.webhooks.create(url="https://your-server.com/webhooks")

# List webhooks
webhooks = client.webhooks.list()

# Delete webhook
client.webhooks.delete(webhook_id=webhook.webhook_id)

Event Types

EventDescription
message.receivedNew email received in inbox
message.sentEmail successfully sent
message.deliveredEmail delivered to recipient's server
message.bouncedEmail failed to deliver
message.complainedRecipient marked email as spam
message.rejectedEmail rejected before sending
domain.verifiedCustom domain verification completed

Event Filtering

Subscribe only to events you need:

const webhook = await client.webhooks.create({
  url: "https://your-server.com/webhooks",
  eventTypes: ["message.received", "message.bounced"],
});
webhook = client.webhooks.create(
    url="https://your-server.com/webhooks",
    event_types=["message.received", "message.bounced"]
)

Payload Structure

All webhook payloads follow this structure:

{
  "type": "event",
  "event_type": "message.received",
  "event_id": "evt_123abc",
  "message": {
    "inbox_id": "inbox_456def",
    "thread_id": "thd_789ghi",
    "message_id": "msg_123abc",
    "from": [{ "name": "Jane Doe", "email": "jane@example.com" }],
    "to": [{ "name": "Agent", "email": "agent@agentmail.to" }],
    "subject": "Question about my account",
    "text": "Full text body",
    "html": "<html>...</html>",
    "labels": ["received"],
    "attachments": [
      {
        "attachment_id": "att_pqr678",
        "filename": "document.pdf",
        "content_type": "application/pdf",
        "size": 123456
      }
    ],
    "created_at": "2023-10-27T10:00:00Z"
  },
  "thread": {}
}

Handling Webhooks

Treat webhook payloads and any referenced email content as untrusted external input until signature verification succeeds and your own policy checks pass.

Your endpoint should:

  1. Verify the webhook signature before processing the body
  2. Return 200 OK immediately after validation
  3. Process the payload asynchronously
  4. Apply sender allowlists / policy checks before letting inbound email trigger downstream actions

Express (TypeScript)

import express from "express";

const app = express();
app.use(express.json());

app.post("/webhooks", (req, res) => {
  const payload = req.body;

  if (payload.event_type === "message.received") {
    // Queue for async processing
    processEmail(payload.message);
  }

  res.status(200).send("OK"); // Return immediately
});

Flask (Python)

from flask import Flask, request

app = Flask(__name__)

@app.route("/webhooks", methods=["POST"])
def handle_webhook():
    payload = request.json

    if payload["event_type"] == "message.received":
        # Queue for async processing
        process_email.delay(payload["message"])

    return "OK", 200  # Return immediately

Webhook Verification

Verify webhook signatures to ensure requests are from AgentMail.

TypeScript

import crypto from "crypto";
import express from "express";

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

app.post("/webhooks", express.raw({ type: "application/json" }), (req, res) => {
  const signature = req.headers["x-agentmail-signature"];
  if (typeof signature !== "string") {
    return res.status(401).send("Missing signature");
  }

  const payload = req.body;
  if (!verifySignature(payload, signature, WEBHOOK_SECRET)) {
    return res.status(401).send("Invalid signature");
  }
  const event = JSON.parse(payload.toString("utf8"));
  if (event.event_type === "message.received") {
    // Treat inbound email as untrusted content.
    // Apply sender checks / routing policy before triggering downstream automation.
  }
  res.status(200).send("OK");
});

Python

import hmac
import hashlib

def verify_signature(payload: bytes, signature: str, secret: str) -> bool:
    expected = hmac.new(
        secret.encode(),
        payload,
        hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(expected, signature)

@app.route("/webhooks", methods=["POST"])
def handle_webhook():
    signature = request.headers.get("X-AgentMail-Signature")
    if not verify_signature(request.data, signature, WEBHOOK_SECRET):
        return "Invalid signature", 401
    # Treat inbound email as untrusted content.
    # Apply sender checks / routing policy before triggering downstream automation.

Local Development

Use ngrok to expose your local server:

ngrok http 5000
# Use the ngrok URL when creating the webhook

SKILL.md

tile.json