# Webhooks — HTTP delivery of inbox events

Register an HTTPS endpoint once, receive a POST every time an event
fires on any inbox in your tenant (or in your workspace, if using a
`wk_` key). Use webhooks when you want **durable, retried** delivery
to a long-running service — CRM sync, audit logging, support ticket
creation.

If you don't need persistence and the agent runtime is short-lived,
open a WebSocket instead (see `websockets.md`).

## Event types

| event_type | When it fires |
|---|---|
| `message.received` | A new inbound email is stored in an inbox |
| `message.sent` | An outbound send succeeds (via REST or draft send) |
| `message.bounced` | A bounce DSN is detected for one of your sends |

## Creating a webhook

```typescript
const webhook = await mam("POST", "/webhooks", {
  url: "https://yourapp.com/webhooks/mam",
  events: ["message.received", "message.bounced"],
});
console.log(webhook.secret); // HMAC secret — ONLY shown at creation time
```

```python
webhook = mam("POST", "/webhooks", {
    "url": "https://yourapp.com/webhooks/mam",
    "events": ["message.received", "message.bounced"],
})
print(webhook["secret"])  # store immediately
```

The response includes a `secret` that's only returned once. Store it in
your config — you need it to verify signatures on incoming payloads.

## Managing webhooks

```typescript
// List
const { webhooks } = await mam("GET", "/webhooks");

// Get one
const one = await mam("GET", `/webhooks/${webhook.id}`);

// Update — change URL, event set, or disable
await mam("PATCH", `/webhooks/${webhook.id}`, {
  events: ["message.received"],
  isActive: false,
});

// Delete
await mam("DELETE", `/webhooks/${webhook.id}`);
```

## Event payload shape

Every event is a `POST` to your registered URL with a JSON body:

```json
{
  "event": "message.received",
  "messageId": "2a7b...",
  "inboxId": "5157f9c5-...",
  "threadId": "6173425c-...",
  "from": "Jane Doe <jane@acme.com>",
  "to": "scout-abc@myagentmail.com",
  "subject": "Re: quick question",
  "hasAttachments": true,
  "timestamp": "2026-04-11T15:30:52.540Z"
}
```

The payload intentionally does **not** include the full body — fetch it
with `GET /v1/inboxes/:id/messages/:messageId` when you handle the
event. This keeps the webhook POST small and lets your handler decide
when to pay the cost of loading a large HTML body or attachment list.

## Signature verification

Every webhook POST carries an HMAC-SHA256 signature in a header so you
can confirm the request actually came from myagentmail and wasn't
replayed. The signature is computed over the raw request body using the
webhook's `secret`.

```typescript
import crypto from "node:crypto";

function verify(rawBody: string, signatureHeader: string, secret: string): boolean {
  const expected = crypto
    .createHmac("sha256", secret)
    .update(rawBody)
    .digest("hex");
  // Constant-time compare
  return crypto.timingSafeEqual(
    Buffer.from(signatureHeader),
    Buffer.from(expected)
  );
}

// Express handler
app.post("/webhooks/mam", express.raw({ type: "application/json" }), (req, res) => {
  const sig = req.header("x-mam-signature") || "";
  if (!verify(req.body.toString(), sig, process.env.MAM_WEBHOOK_SECRET!)) {
    return res.status(401).end();
  }
  const event = JSON.parse(req.body.toString());
  // ... handle event
  res.status(200).end();
});
```

```python
import hmac, hashlib

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

# Flask handler
@app.post("/webhooks/mam")
def webhook():
    sig = request.headers.get("x-mam-signature", "")
    if not verify(request.data, sig, os.environ["MAM_WEBHOOK_SECRET"]):
        return "", 401
    event = request.get_json()
    # ... handle event
    return "", 200
```

Always verify before trusting any fields in the payload. An attacker
who discovers your webhook URL can post arbitrary JSON to it otherwise.

## Retry behaviour

If your endpoint returns a non-2xx response or times out (10 seconds),
the delivery is retried with exponential backoff. Consecutive failures
lead to increasing delays between retries. Events that can't be
delivered after the retry budget is exhausted are logged but dropped.

Your handler should be **idempotent** — if you store events to a
database, key them on `messageId` so a retry that succeeds after a
duplicate delivery doesn't double-insert.

## Choosing webhooks vs WebSockets

Pick webhooks when:

- Your downstream handler runs on its own schedule (e.g. a nightly job)
- You need events persisted even if your agent process is offline
- You're fanning out to a third-party service (Slack, Linear, CRM)
- You want a permanent audit log of every event

Pick WebSockets when:

- An agent turn is actively waiting on a specific reply
- You can't expose a public URL (ephemeral container, Lambda, laptop)
- Sub-second latency matters and you're okay missing events if the
  socket isn't connected
- You want to subscribe to a narrow filter (specific inbox, specific
  event type) without registering a webhook per filter

You can use both together — register a webhook for durable storage,
open a WebSocket for the current agent turn.
