# AgentMail Webhooks

[AgentMail](https://agentmail.to) gives AI agents their own email inboxes. It sends webhooks through Svix when mail arrives, when outbound mail is sent, delivered, bounced or complained about, when a domain is verified, and when calendar events change.

## When to Use This Skill

* How do I receive AgentMail webhooks?
* How do I verify an AgentMail webhook signature (`svix-signature`)?
* How do I trigger my agent when an email arrives (`message.received`)?
* How do I handle `message.bounced` or `message.complained` for outbound mail?
* Why is my AgentMail webhook signature verification failing?
* Why am I processing the same AgentMail event twice?

## Verification (core)

| Header | Value |
| --- | --- |
| `svix-id` | Message ID. Same across retries to one webhook, **different** for webhooks at the organization, pod and inbox levels |
| `svix-timestamp` | Unix seconds when the message was sent |
| `svix-signature` | Space-delimited list of `v1,<base64>` signatures |

Signed content is `{svix-id}.{svix-timestamp}.{raw body}`, HMAC-SHA256, base64. The key is the base64-**decoded** part of the secret after `whsec_`. AgentMail recommends the official `svix` library (npm and PyPI), which also enforces a 5-minute timestamp tolerance and compares in constant time.

```javascript
import { Webhook } from 'svix'; // svix 2.x is ESM-only

// rawBody: the exact bytes received (express.raw() / request.text()), NOT re-serialised JSON
function verifyAgentMailWebhook(rawBody, headers) {
  const secret = process.env.AGENTMAIL_WEBHOOK_SECRET; // whsec_...
  if (!secret) throw new Error('AGENTMAIL_WEBHOOK_SECRET not set'); // fail closed → 500
  new Webhook(secret).verify(rawBody, {
    'svix-id': headers['svix-id'],
    'svix-timestamp': headers['svix-timestamp'],
    'svix-signature': headers['svix-signature'],
  }); // throws WebhookVerificationError → 400
  return JSON.parse(rawBody.toString('utf8')); // svix 2.x verify() returns nothing
}

```

Python: `from svix.webhooks import Webhook`, then `Webhook(secret).verify(raw_body, headers)` followed by `json.loads(raw_body)`.

> **svix 2.x gotcha:** `verify()` no longer returns the parsed payload (JS since 2.2, Python since 2.0). AgentMail's docs show `const msg = wh.verify(...)`, which gives `undefined`/`None` on current svix. Verify, then parse the raw body yourself.

Manual verification (no SDK) is in [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/agentmail-webhooks/references/verification.md).

> **For complete handlers with tests**, see [examples/express/](https://github.com/hookdeck/webhook-skills/tree/main/skills/agentmail-webhooks/examples/express/), [examples/nextjs/](https://github.com/hookdeck/webhook-skills/tree/main/skills/agentmail-webhooks/examples/nextjs/), [examples/fastapi/](https://github.com/hookdeck/webhook-skills/tree/main/skills/agentmail-webhooks/examples/fastapi/).

## Deduplicate on `event_id`, Not `svix-id`

Delivery is at least once. The payload's `event_id` is the same on every retry **and** on every webhook that receives the event. `svix-id` changes between webhooks registered at different levels (organization, pod, inbox), so it lets duplicates through when one event matches several webhooks.

```json
{ "type": "event", "event_type": "message.received", "event_id": "c81e728d9d4c2f636f067f89cc14862c",
  "message": { "inbox_id": "inbox_456def", "message_id": "<abc123@agentmail.to>", "subject": "...", "...": "..." },
  "thread":  { "thread_id": "thd_789ghi", "message_count": 1, "...": "..." } }

```

The discriminator is `event_type` (not `type`, which is always `"event"`). Treat `event_id` as an opaque string; its format varies by event type.

## Common Event Types

| Event | Fired when | Payload object |
| --- | --- | --- |
| `message.received` | A new email arrives in an inbox | `message` + `thread` |
| `message.sent` | An outbound message leaves AgentMail | `send` |
| `message.delivered` | The recipient's mail server accepts it | `delivery` |
| `message.bounced` | Delivery fails (`type`, `sub_type`, `recipients`) | `bounce` |
| `message.complained` | A recipient marks it as spam | `complaint` |
| `message.rejected` | Rejected before sending (`reason`) | `reject` |
| `message.opened` | First open of a tracked message | `open` |
| `domain.verified` | A custom domain is verified | `domain` |

Also: `message.received.spam`, `message.received.blocked`, `message.received.unauthenticated` (same shape as `message.received`, opt-in only), `message.security.completed`, `message.security.review`, `message.security.override` (Agent Armor), and `calendar.event.created|updated|deleted|responded|starting|ending`. See [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/agentmail-webhooks/references/overview.md) for all 20.

## Delivery Semantics

* Return a `2xx` within **15 seconds**. Redirects count as failures. Acknowledge quickly and process in the background.
* Retries: immediately, 5s, 5m, 30m, 2h, 5h, 10h, 10h (8 attempts over ~27.5h). An endpoint failing for 5 days is disabled (`enabled: false`) without notification.
* Events aren't ordered. Order by the timestamps in the payload.

## Environment Variables

```bash
AGENTMAIL_WEBHOOK_SECRET=whsec_xxxxx  # Console → Webhooks, or client.webhooks.get(id).secret

```

## Local Development

```bash
# Express / Next.js (use 8000 for FastAPI). No install or account required.
npx hookdeck-cli listen 3000 agentmail --path /webhooks/agentmail

```

Register the printed URL with `client.webhooks.create(url=..., event_types=["message.received"])` or in the AgentMail Console. `event_types` is required.

## Reference Materials

* [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/agentmail-webhooks/references/overview.md) - All event types and payload shapes
* [references/setup.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/agentmail-webhooks/references/setup.md) - Creating webhooks (console, SDK, CLI), scoping to pods/inboxes, getting the secret
* [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/agentmail-webhooks/references/verification.md) - SDK and manual verification, gotchas, debugging