> For clean Markdown content of this page, append .md to this URL. For the complete documentation index, see https://docs.agentmail.to/llms.txt. For full content including API reference and SDK examples, see https://docs.agentmail.to/llms-full.txt.

# Webhooks Overview

> 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](/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](/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](https://docs.agentmail.to/api-reference/webhooks/update-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](/core-concepts/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 title="CLI"
# 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](/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 title="CLI"
# fetch the full message after receiving a webhook
agentmail inboxes:messages get \
  --inbox-id <inbox_id> \
  --message-id <message_id>
```

```json Webhook Payload
{
  "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](/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 title="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 title="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.