> ## Documentation Index
> Fetch the complete documentation index at: https://docs.useanima.sh/llms.txt
> Use this file to discover all available pages before exploring further.

# Webhooks

> Configure webhooks to receive real-time updates for email, SMS, and call events — with signed, replay-protected deliveries.

# Webhooks

Subscribe to real-time events like incoming emails, delivery failures, and completed calls.

## Configuration

Configure webhooks in the dashboard, via the API, the CLI, the `webhook_set` MCP tool, or any SDK. Each delivery is a JSON POST to your endpoint:

```json theme={null}
{
  "event": "message.received",
  "occurredAt": "2026-07-28T12:00:00.000Z",
  "messageId": "cme9x2k1p0001s601abcdefgh",
  "agentId": "cme9x2k1p0000s601ijklmnop",
  "channel": "email",
  "direction": "INBOUND",
  "fromAddress": "user@example.com",
  "toAddress": "support-agent@agents.useanima.sh",
  "threadId": "cme9x2k1p0002s601qrstuvwx",
  "subject": "Hello",
  "spam": false
}
```

## Event Types

Subscribing to a name that isn't on this list is accepted but never fires, so copy them exactly. `GET /webhooks/event-types` returns the same list from the live API.

| Event Name | Fires when |
| - | - |
| `message.received` | Email or SMS arrives for one of your agents. |
| `message.received.auto` | Inbound mail detected as automated (auto-reply, out-of-office). Fired *instead of* `message.received`, so subscribe to it explicitly if you want it. |
| `message.sent` | An outbound email or SMS is accepted for delivery. |
| `message.failed` | An outbound message failed to send, or the recipient complained. |
| `message.bounced` | An outbound email bounced. |
| `message.loop_detected` | Repeated sends to the same address tripped the velocity breaker; the message is held for approval. |
| `agent.created` | An agent is created. |
| `agent.updated` | An agent is updated. |
| `agent.deleted` | An agent is deleted. |
| `phone.provisioned` | A number is attached to an agent. |
| `phone.released` | A number is released from an agent. |
| `call.started` | A voice call begins. |
| `call.ended` | A voice call completes. |
| `call.summary.ready` | The post-call summary finishes processing. |
| `call.score.ready` | The post-call quality score finishes processing. |
| `call.security.alert` | A call's security scan raises an alert. |
| `call.security.scan.ready` | A call's security scan finishes. |
| `a2a.task.received` | Another agent sent one of your agents an A2A task. |
| `vault.credential.refresh_failed` | A stored OAuth credential could not be refreshed and is marked `[needs reauth]`. The agent cannot authenticate to that provider until a human re-consents. |

### Subscriptions are org-scoped

A subscription belongs to your **organization**, not to a single agent — one endpoint receives the events for every agent you run. There is no `agentId` on a subscription; use the `agentId` in the payload to tell agents apart.

### Wildcards

A bare `*` matches everything. Otherwise `*` matches exactly **one** dot-separated segment, and `**` matches across segments. This trips people up on the three-segment names:

| Pattern | `call.ended` | `call.security.alert` |
| - | - | - |
| `call.*` | matches | **no match** |
| `call.**` | matches | matches |
| `*` | matches | matches |

The same applies to `message.*`, which does **not** match `message.received.auto`.

### Payload shape

Flat JSON — there is no `data` envelope to unwrap. Every event carries `event` and `occurredAt`; message events add `messageId`, `agentId`, `channel`, `direction`, `fromAddress`, `toAddress`, `threadId`, and (for email) `subject` and `spam`. That is enough addressing to reply without a second call. The message **body is not included** — fetch `GET /v1/messages/{id}` when you need the content.

## Signing secret

When you create a webhook, the API returns a `secret` **once**, in the create response. Store it securely — read endpoints (`GET /webhooks` and `GET /webhooks/{id}`) never return it again. If you lose it, rotate it:

```bash theme={null}
curl -X POST https://api.useanima.sh/v1/webhooks/{id}/rotate-secret \
  -H "Authorization: Bearer mk_..."
# → { "id": "wh_...", "secret": "<new secret, shown once>" }
```

Rotating immediately invalidates the previous secret.

## Verifying deliveries

Every delivery carries a signature and a timestamp so you can confirm it came from Anima and reject replays:

| Header | Description |
| - | - |
| `X-Anima-Signature` | `v1=<hex>` — HMAC-SHA256 of `{timestamp}.{rawBody}`, keyed by your signing secret. |
| `X-Anima-Timestamp` | ISO-8601 time the delivery was signed; bound into the signature. |
| `X-Anima-Event` | The event name (e.g. `message.received`). |
| `X-Anima-Delivery-Id` | Stable id for this delivery, unchanged across retries. |

Recompute the HMAC over `{timestamp}.{rawBody}`, compare it in constant time, and reject deliveries whose timestamp falls outside a tolerance window (for example, 5 minutes). The timestamp is part of the signed content specifically so you can stop replays.

```ts theme={null}
import { createHmac, timingSafeEqual } from "node:crypto";

const TOLERANCE_MS = 5 * 60 * 1000;

export function verifyAnimaWebhook(
  rawBody: string,
  headers: { "x-anima-signature": string; "x-anima-timestamp": string },
  secret: string,
): boolean {
  const timestamp = headers["x-anima-timestamp"];
  // Reject stale or replayed deliveries.
  if (Math.abs(Date.now() - Date.parse(timestamp)) > TOLERANCE_MS) return false;

  const provided = headers["x-anima-signature"].replace(/^v1=/, "");
  const expected = createHmac("sha256", secret)
    .update(`${timestamp}.${rawBody}`)
    .digest("hex");

  const a = Buffer.from(provided, "hex");
  const b = Buffer.from(expected, "hex");
  return a.length === b.length && timingSafeEqual(a, b);
}
```

<Warning>
  Verify against the **raw request body**, before any JSON parse or re-serialize — re-encoding can change bytes and break the signature.
</Warning>

## Advanced settings

The `X-Anima-Signature` HMAC already proves a delivery came from Anima. On top of it, you can have Anima present a credential your endpoint checks, and control how fast it delivers.

### Endpoint authentication

Handy when your gateway expects a header rather than a signature. This is in addition to the HMAC.

| Type | What Anima sends |
| - | - |
| `bearer` | `Authorization: Bearer <token>` |
| `basic` | `Authorization: Basic <base64(username:password)>` |
| `custom_header` | A header you name, e.g. `X-My-Secret: <value>` |

The credential is write-only — set on create or update, never returned by a read, encrypted at rest.

### Delivery throttling and retries

* **`rateLimitPerMinute`** — cap deliveries per minute to a single endpoint. Over-limit deliveries defer to the next window rather than dropping.
* **`maxAttempts`** — max delivery attempts before dead-lettering (default 3). Retries use exponential backoff, and an endpoint that keeps failing is auto-disabled.

Set these when you create or update a webhook — via the API, the `webhook_set` MCP tool, the CLI, or any SDK:

```bash theme={null}
# CLI
anima webhook create \
  --url https://example.com/hooks/anima \
  --events message.received,message.sent \
  --auth-config '{"type":"bearer","token":"your-endpoint-token"}' \
  --rate-limit-per-minute 120 \
  --max-attempts 5
```

```python theme={null}
# Python
from anima import Anima, WebhookAuthBearer

anima = Anima(api_key="ak_...")
anima.webhooks.create(
    url="https://example.com/hooks/anima",
    events=["message.received", "message.sent"],
    auth_config=WebhookAuthBearer(token="your-endpoint-token"),
    rate_limit_per_minute=120,
    max_attempts=5,
)
```

```ts theme={null}
// TypeScript
import { Anima } from "@anima-labs/sdk";

const anima = new Anima({ apiKey: "ak_..." });
await anima.webhooks.create({
  url: "https://example.com/hooks/anima",
  events: ["message.received", "message.sent"],
  authConfig: { type: "bearer", token: "your-endpoint-token" },
  rateLimitPerMinute: 120,
  maxAttempts: 5,
});
```

```go theme={null}
// Go
rateLimit, maxAttempts := 120, 5
client.Webhooks.Create(ctx, anima.CreateWebhookParams{
    URL:                "https://example.com/hooks/anima",
    Events:             []anima.WebhookEventType{anima.WebhookEventMessageReceived},
    AuthConfig:         anima.NewBearerAuth("your-endpoint-token"),
    RateLimitPerMinute: &rateLimit,
    MaxAttempts:        &maxAttempts,
})
```

The other schemes work the same way: `basic` (username + password) and `custom_header` (a header name + value) — in the SDKs, `WebhookAuthBasic` / `WebhookAuthCustomHeader` (Python), the matching `{ type: "basic", … }` union member (TypeScript), or `anima.NewBasicAuth` / `anima.NewCustomHeaderAuth` (Go). Pass `{"type":"none"}` on update to remove authentication.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.