Skip to content

Webhooks Overview ​

Fresh 🌱

Learn how to use Webhooks to build responsive, event-driven email agents with AgentMail.

Webhooks are the best way to get real-time information about what's happening with your emails. Instead of constantly asking the AgentMail API if there's a new email (a process called polling), you can register a URL, and we will send you a POST request with the details as soon as an event happens.

This event-driven approach is more efficient and allows you to build fast, responsive agents that can react instantly to incoming messages.

Why Use Webhooks? ​

  • Real-Time Speed: Build conversational agents that can reply to incoming emails in seconds.
  • Efficiency: Eliminates the need for constant polling, which saves you computational resources and simplifies your application logic.

If you don't want to expose a public URL or set up ngrok, WebSockets let you receive the same events over a persistent connection with no external tooling required.

Available Events ​

AgentMail supports ten webhook event types. When creating a webhook, you can subscribe to specific events or receive all standard events. See Webhook Events for full payload details.

Message events:

  • message.received β€” New email received and processed in one of your inboxes
  • message.received.spam β€” A message was received and classified as spam (requires label_spam_read permission)
  • message.received.blocked β€” A message was received and matched a block list entry (requires label_blocked_read permission)
  • message.received.unauthenticated β€” A message was received without authentication headers, so AgentMail could not verify whether it was authenticated
  • message.sent β€” Message successfully sent from your inbox
  • message.delivered β€” Delivery confirmed by the recipient's mail server
  • message.bounced β€” Message failed to deliver and bounced back
  • message.complained β€” Recipient marked your message as spam
  • message.rejected β€” Message rejected before send (validation or policy)

Domain events:

  • domain.verified β€” Custom domain successfully verified

Spam, blocked, and unauthenticated events are excluded by default. To receive them, explicitly include message.received.spam, message.received.blocked, or message.received.unauthenticated in the event_types list when creating a webhook. These messages are no longer sent as message.received.

Updating event_types on a webhook ​

When you update a webhook, you can change which events it receives by sending event_types on PATCH:

  • Non-empty array: Replaces the webhook's subscribed event types in full (same idea as create: you set the whole list, not a diff). If you send only one type, the webhook will only receive that type afterward.
  • Omitted or empty array: Leaves the current event types unchanged. You cannot clear all subscriptions by sending an empty list.

Subscribing to message.received.spam, message.received.blocked, or message.received.unauthenticated on update still requires the same label permissions as on create.

The Webhook Workflow ​

The process is straightforward:

This is a public URL on your server that can accept POST requests. For local development, a tool like ngrok is perfect for creating a secure, public URL that tunnels to your local machine. Your endpoint should immediately return a 200 OK response to acknowledge receipt and process the payload in the background to avoid timeouts.

You can register your URL using the AgentMail API. When you create a webhook, you'll specify your endpoint's URL as well as event types you want to receive.

python
client.webhooks.create(
    url="https://<your-ngrok-url>.ngrok-free.app/webhooks",
    event_types=["message.received", "message.sent"],
)
typescript
await client.webhooks.create({
    url: "https://<your-ngrok-url>.ngrok-free.app/webhooks",
    eventTypes: ["message.received", "message.sent"],
});
bash
# register a webhook endpoint
agentmail webhooks create \
  --url https://<your-ngrok-url>.ngrok-free.app/webhooks \
  --event-type message.received \
  --event-type message.sent

Specify which events to receive; omit event_types to subscribe to all standard event types. Spam, blocked, and unauthenticated events must always be explicitly included.

When an event occurs (e.g. a new message is received, a message is delivered, or a domain is verified), AgentMail sends a POST request with a JSON payload to your registered URL.

Payload Structure ​

When AgentMail sends a webhook, the payload includes event_type and event_id, plus event-specific data. The example below shows a message.received payload; other events use different top-level objects (send, delivery, bounce, etc.). See Webhook Events for each event's payload shape.

Payload size limit ​

Webhook payloads are capped at 1 MB. When a message exceeds this limit, AgentMail omits the text and html fields from the webhook payload to reduce size. All other metadata is still included. Inline images embedded in the HTML (such as base64-encoded data URIs) count toward the payload size and are a common reason for the limit being reached.

Omitted content is always available through the API. After receiving a webhook, fetch the full message to access the complete body and attachment data:

python
# fetch the full message after receiving a webhook
message = client.inboxes.messages.get(
    inbox_id=payload["message"]["inbox_id"],
    message_id=payload["message"]["message_id"],
)
text_body = message.text
html_body = message.html
typescript
// fetch the full message after receiving a webhook
const message = await client.inboxes.messages.get(
  payload.message.inbox_id,
  payload.message.message_id,
);
const textBody = message.text;
const htmlBody = message.html;
bash
# fetch the full message after receiving a webhook
agentmail inboxes:messages get \
  --inbox-id <inbox_id> \
  --message-id <message_id>
json
{
  "event_type": "message.received",
  "event_id": "evt_123abc...",
  "message": {
    "from_": ["sender@example.com"],
    "organization_id": "org_abc123...",
    "inbox_id": "inbox_def456...",
    "thread_id": "thd_ghi789...",
    "message_id": "<jkl012@agentmail.to>",
    "labels": ["received"],
    "timestamp": "2023-10-27T10:00:00Z",
    "reply_to": ["reply-to@example.com"],
    "to": ["recipient@example.com"],
    "cc": ["cc-recipient@example.com"],
    "bcc": ["bcc-recipient@example.com"],
    "subject": "Email Subject",
    "preview": "A short preview of the email text...",
    "text": "The full text body of the email.",
    "html": "<html>...</html>",
    "attachments": [
      {
        "attachment_id": "att_pqr678...",
        "filename": "document.pdf",
        "content_type": "application/pdf",
        "size": 123456,
        "inline": false
      }
    ],
    "in_reply_to": "<parent456@agentmail.to>",
    "references": ["<ref001@agentmail.to>", "<ref002@agentmail.to>"],
    "sort_key": "some-sort-key",
    "updated_at": "2023-10-27T10:00:05Z",
    "created_at": "2023-10-27T10:00:00Z"
  }
}

Field Descriptions ​

  • event_type (string): The event type (e.g. message.received, message.sent, message.delivered). Payload structure varies by eventβ€”see Webhook Events for each event's shape.
  • event_id (string): A unique identifier for this specific event delivery.
  • message (object): A dictionary containing the full details of the received email message.
    • from_ (array<string>): The sender's email address. Note the trailing underscore to avoid conflict with the Python keyword.
    • organization_id (string): The ID of your organization.
    • inbox_id (string): The ID of the inbox that received the message.
    • thread_id (string): The ID of the conversation thread.
    • message_id (string): The unique ID of this specific message.
    • labels (array<string>): Labels associated with the message (e.g., received, sent).
    • subject (string): The subject line of the email.
    • preview (string): A short plain-text preview of the email body.
    • text (string): The plain-text body of the email. May be omitted when the webhook payload exceeds the 1 MB size limit. Fetch the full message via the API if this field is missing.
    • html (string): The HTML body of the email, if present. May be omitted when the webhook payload exceeds the 1 MB size limit. Fetch the full message via the API if this field is missing.
    • attachments (array<object>): A list of attachment metadata, each with its own attachment_id, filename, content_type, size, and inline status. Attachment content is not included in the webhook payload; download attachments through the API.
    • in_reply_to (string): The message_id of the email this message is a reply to, if applicable.

Copy for Cursor / Claude ​

Copy one of the blocks below into Cursor or Claude for complete Webhooks API knowledge in one shot.

python
"""
AgentMail Webhooks β€” copy into Cursor/Claude.

Setup: pip install agentmail python-dotenv. Set AGENTMAIL_API_KEY in .env.
Return 200 immediately; process payload in background.

API reference:
- webhooks.create(url, event_types?, inbox_ids?, pod_ids?, client_id?)
- webhooks.get(webhook_id), webhooks.list(limit?, page_token?)
- webhooks.update(webhook_id, add_inbox_ids?, remove_inbox_ids?, add_pod_ids?, remove_pod_ids?, event_types?)
- webhooks.delete(webhook_id)

Events: message.received, message.received.spam, message.received.blocked, message.received.unauthenticated, message.sent, message.delivered, message.bounced, message.complained, message.rejected, domain.verified
Payload: event_type, event_id, plus message/send/delivery/bounce/complaint/reject/domain. Verify with Svix (webhook.secret).
Note: message.received.spam, message.received.blocked, and message.received.unauthenticated are excluded by default. To receive them, explicitly include them in event_types and ensure the API key has label_spam_read / label_blocked_read permissions for spam and blocked events.
On update, non-empty event_types replaces the subscribed list in full (omit or [] to leave types unchanged).
"""
import os
from dotenv import load_dotenv
from agentmail import AgentMail

load_dotenv()
client = AgentMail(api_key=os.getenv("AGENTMAIL_API_KEY"))

wh = client.webhooks.create(url="https://your-server.com/webhooks", event_types=["message.received"], client_id="my-webhook-v1")
all_wh = client.webhooks.list()
secret = client.webhooks.get(wh.webhook_id).secret
typescript
/**
 * AgentMail Webhooks β€” copy into Cursor/Claude.
 *
 * Setup: npm install agentmail dotenv. Set AGENTMAIL_API_KEY in .env.
 * Return 200 immediately; process payload in background.
 *
 * API reference:
 * - webhooks.create({ url, eventTypes?, inboxIds?, podIds?, clientId? })
 * - webhooks.get(webhookId), webhooks.list({ limit?, pageToken? })
 * - webhooks.update(webhookId, { addInboxIds?, removeInboxIds?, addPodIds?, removePodIds?, eventTypes? })
 * - webhooks.delete(webhookId)
 *
 * Events: message.received, message.received.spam, message.received.blocked, message.received.unauthenticated, message.sent, message.delivered, message.bounced, message.complained, message.rejected, domain.verified
 * Note: message.received.spam, message.received.blocked, and message.received.unauthenticated are excluded by default. To receive them, explicitly include them in eventTypes and ensure the API key has label_spam_read / label_blocked_read permissions for spam and blocked events.
 * On update, non-empty eventTypes replaces the subscribed list in full (omit or [] to leave types unchanged).
 * Verify with Svix using webhook.secret. Use express.raw() for bodyβ€”signature needs raw payload.
 */
import { AgentMailClient } from "agentmail";
import "dotenv/config";

const client = new AgentMailClient({ apiKey: process.env.AGENTMAIL_API_KEY! });

async function main() {
  const wh = await client.webhooks.create({
    url: "https://your-server.com/webhooks",
    eventTypes: ["message.received"],
    clientId: "my-webhook-v1",
  });
  const allWh = await client.webhooks.list();
  const secret = (await client.webhooks.get(wh.webhookId)).secret;
}
main();

Next Steps ​

Explore the full list of available event types and their data payloads.

Learn how to verify webhook signatures to secure your endpoints.

Receive events over a persistent connection with no public URL required.

Build a fully deployable, event-driven agent that can respond to emails in real time.