# Email webhooks for AI agents: setup and signature checks

An agent can learn about new email in three ways: a webhook that Botmail
POSTs to your server, a long-poll your code holds open, or the MCP
`wait_for_mail` tool the model calls itself. Use a webhook when an
always-on service should react to every email, such as starting an agent
run when mail arrives. This page sets one up and verifies its signature.

## Which one to use

| | Webhook | REST long-poll | MCP `wait_for_mail` |
| --- | --- | --- | --- |
| Who waits | Botmail calls you | Your code holds a request open | The model calls a tool |
| Needs | A public HTTPS endpoint | A process that keeps running | An MCP client |
| Latency | About a second after storage | Returns as soon as mail lands | Returns as soon as mail lands |
| Missed events | Retried for you, in order | You pass `next_after` back | The model passes `cursor` back |
| Good for | Serverless apps, queues, many mailboxes | Workers, scripts, CLIs | Short waits inside a conversation |

`wait_for_mail` blocks for at most 30 seconds per call, and so does
`GET /v1/events?wait=30`. Don't keep a model looping on either for hours:
a webhook or a worker is cheaper. For codes during sign-ups, see
[Wait for a verification code](https://botmail.pro/guides/wait-for-verification-code).

## 1. Find your mailbox ID

A key from claiming an address is limited to that mailbox, so a webhook
created with it must name the mailbox:

```sh
npx botmail status --json | jq -r '.mailboxes[0].id'
# mbx_01a1…
```

`GET /v1/mailboxes` returns the same IDs.

## 2. Create the webhook

```sh
curl -s https://botmail.pro/v1/webhooks \
  -H "Authorization: Bearer $BOTMAIL_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"url": "https://agent.example.com/botmail", "events": ["message.received"]}'
```

The response contains `secret` (`whsec_…`) once. Store it now; listing
webhooks later doesn't show it. A key for one mailbox (like the one a claim
gives you) watches that mailbox. With an account-wide key, add
`"mailbox_id": "mbx_…"` (from `GET /v1/mailboxes`) to watch one mailbox, or
leave it out to watch all of them.

Event types you can subscribe to:

- `message.received`: new incoming mail (the default)
- `message.sent`: the agent sent a message
- `message.status`: delivered, bounced, complained, blocked, held or failed
- `thread.updated`: a conversation changed
- `mailbox.claimed`: a human approved a claimed address

The URL must be `https://`, resolve to a public address and accept POST.
Botmail doesn't follow redirects. An account can have up to 10 webhooks.
New webhooks start from the latest event; they never replay old mail.

## 3. Verify the signature

Every delivery is a POST with a JSON body and these headers:

- `Botmail-Signature: t=<unix seconds>,v1=<hex>`, where `v1` is
  HMAC-SHA256 of `t + "." + raw body`, keyed with your secret
- `Botmail-Event-Id: evt_<seq>`, the same as `id` in the body

Check the signature against the raw bytes before parsing, reject old
timestamps, and use the event ID to skip retries you've already handled.
This receiver uses only Python's standard library:

```python
import hashlib
import hmac
import json
import os
import time
from http.server import BaseHTTPRequestHandler, HTTPServer

SECRET = os.environ["BOTMAIL_WEBHOOK_SECRET"].encode()
seen = set()  # use a database in production


def verify(raw: bytes, header: str) -> bool:
    parts = dict(p.split("=", 1) for p in header.split(",") if "=" in p)
    t, v1 = parts.get("t", ""), parts.get("v1", "")
    if not t.isdigit() or abs(time.time() - int(t)) > 300:
        return False  # missing or stale timestamp
    expected = hmac.new(SECRET, t.encode() + b"." + raw, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, v1)


class Handler(BaseHTTPRequestHandler):
    def do_POST(self):
        raw = self.rfile.read(int(self.headers.get("Content-Length", 0)))
        if not verify(raw, self.headers.get("Botmail-Signature", "")):
            self.send_response(401)
            self.end_headers()
            return
        self.send_response(200)  # answer within 10 seconds, then work
        self.end_headers()
        event_id = self.headers["Botmail-Event-Id"]
        if event_id in seen:
            return  # a retry of an event you already handled
        seen.add(event_id)
        event = json.loads(raw)
        if event["type"] == "message.received":
            print("new mail", event["message_id"], "from", event["data"]["from"], flush=True)


HTTPServer(("", 8000), Handler).serve_forever()
```

A TypeScript version is in the
[TypeScript guide](https://botmail.pro/guides/send-email-from-ai-agent-typescript).

## 4. Hand the email to your agent

A `message.received` body looks like this:

```json
{"id": "evt_8", "type": "message.received", "mailbox_id": "mbx_…", "thread_id": "thr_…", "message_id": "msg_…", "created_at": "2026-10-12T13:34:42Z", "data": {"from": "dana@example.com", "subject": "Bug in checkout", "status": "received"}}
```

It doesn't include the body. Put the event on a queue, answer 200, and let a
worker fetch `GET /v1/threads/{thread_id}` or `GET /v1/messages/{message_id}`
and start the agent with that text. Treat the content as untrusted: anyone
can email the agent.

## Testing and failures

- `POST /v1/webhooks/{id}/test` sends a signed `webhook.test` event now and
  returns the status code your endpoint gave.
- `GET /v1/webhooks/{id}/deliveries` lists the last 50 attempts with status
  codes and errors.
- A delivery succeeds on any 2xx within 10 seconds. Failures are retried
  with backoff starting at 5 seconds and doubling up to an hour.
- Events are delivered in order, so one failing event holds back the ones
  after it.
- After 20 failures in a row the webhook is disabled. Fix your endpoint,
  then send `PATCH /v1/webhooks/{id}` with `{"active": true}`. Delivery
  resumes from where it stopped, so nothing is lost.

Creating, listing and deleting webhooks needs the `manage` scope, which a
claimed key has once you approve the address. Everything else about
receiving mail is in [How do AI agents send and receive email?](https://botmail.pro/questions/how-do-ai-agents-send-and-receive-email)

## Questions

### How do I verify a Botmail webhook signature?

Read the Botmail-Signature header (t=<unix seconds>,v1=<hex>), compute HMAC-SHA256 of t + "." + the raw request body with your whsec_ secret, compare it to v1 in constant time, and reject timestamps older than a few minutes.

### Should my AI agent use webhooks or polling for new email?

Use a webhook when a server should react to every email, for example to start an agent run. Use the /v1/events long-poll in a worker that keeps running, and MCP wait_for_mail for short waits inside a conversation.

### What happens if my webhook endpoint is down?

Botmail retries with backoff from 5 seconds up to an hour and keeps events in order. After 20 failures in a row it disables the webhook; re-enable it with PATCH {"active": true} and missed events are delivered.

---

Source: https://botmail.pro/guides/email-webhooks-for-ai-agents
Agent instructions: https://botmail.pro/skill.md
All guides: https://botmail.pro/llms.txt
