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:
{
"event": "message.received",
"data": {
"id": "msg_12345",
"agent_id": "agent_abc",
"from": "user@example.com",
"subject": "Hello",
"body": "Hi there...",
"timestamp": "2023-10-01T12:00:00Z"
}
}
Event Types
| Event Name | Description |
|---|
message.received | Triggered when an agent receives a new email or SMS. |
message.sent | Triggered when a message is successfully sent. |
message.failed | Triggered when a message fails to send or bounces. |
agent.created | Triggered when a new agent mailbox is created. |
call.ended | Triggered when a voice call completes. |
The full, current list is available from GET /webhooks/event-types.
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:
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.
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);
}
Verify against the raw request body, before any JSON parse or re-serialize — re-encoding can change bytes and break the signature.
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:
# 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
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,
)
// 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
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.