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

# Webhooks

> Subscribe to session and message events; receive inbound email and SMS.

Webhooks let your systems react to what agents do without polling. This page covers **outbound** webhooks — Naive Managed Agents calls your endpoint when subscribed events occur. **Inbound** receive endpoints for email and SMS are live too: messages are verified, stored, and wake the inbox's or number's owning agent — see [Inbound events](https://vetta.sh/docs/identity/inbound.md).

## Outbound webhooks

Subscribe an HTTPS endpoint to a filtered set of [events](https://vetta.sh/docs/concepts/events-and-streaming.md).

```bash CLI theme={"system"}
vetta webhook add \
  --url https://example.com/hooks/vetta \
  --events session.idle,message.completed,budget.exceeded
vetta webhook list
vetta webhook test <webhook_id>
```

```typescript TypeScript theme={"system"}
const hook = await vetta.webhooks.create({
  url: "https://example.com/hooks/vetta",
  events: ["session.idle", "message.completed", "budget.exceeded"],
});
```

### Delivery & verification

Each delivery is a signed JSON **envelope** — not the raw stream event. The envelope is `{ id, type, created_at, data }`, where `data` is the event-specific body (for `session.idle`, the `session_id`, `stop_reason`, `structured_output`, and so on). The `id` is stable across redeliveries, so your handler can dedupe on it.

```json theme={"system"}
{
  "id": "evt_7h...",
  "type": "session.idle",
  "created_at": "2026-08-20T09:00:12Z",
  "data": { "session_id": "ses_4a...", "stop_reason": "end_turn" }
}
```

Verify the signature with the webhook secret before trusting the payload. Each signature is an **HMAC-SHA256 over `{timestamp}.{raw_body}`**, hex-encoded, carried as the **versioned** header `Vetta-Signature: v1=<hex>` beside `Vetta-Timestamp: <unix seconds>`. During a [secret rotation](#rotating-the-secret) the header carries more than one signature (comma-separated, `v1=<old>,v1=<new>`), so a verifier accepts the delivery if **any** listed signature matches. Reject a delivery whose timestamp is more than **300 seconds** old, or a replayed capture verifies.

The SDK does not ship a verifier — verification happens on your server, against the raw bytes, so it is a dozen lines of standard crypto rather than a dependency:

```typescript TypeScript theme={"system"}
import { createHmac, timingSafeEqual } from "node:crypto";

const TOLERANCE_SECONDS = 300;

function verify(rawBody: Buffer, headers: Record<string, string>, secret: string): boolean {
  const timestamp = Number(headers["vetta-timestamp"]);
  if (!Number.isFinite(timestamp)) return false;
  if (Math.abs(Date.now() / 1000 - timestamp) > TOLERANCE_SECONDS) return false;

  const expected = createHmac("sha256", secret)
    .update(`${timestamp}.`)
    .update(rawBody)
    .digest("hex");

  // The header may list several `v1=` signatures during a rotation; any match is a pass.
  return (headers["vetta-signature"] ?? "")
    .split(",")
    .filter((part) => part.startsWith("v1="))
    .some((part) => {
      const got = Buffer.from(part.slice(3), "hex");
      const want = Buffer.from(expected, "hex");
      return got.length === want.length && timingSafeEqual(got, want);
    });
}

// Needs the RAW request body — register this route before any JSON body parser.
app.post("/hooks/vetta", express.raw({ type: "application/json" }), async (req, res) => {
  if (!verify(req.body, req.headers, process.env.VETTA_WEBHOOK_SECRET!)) {
    return res.sendStatus(400);
  }
  const event = JSON.parse(req.body.toString("utf8"));
  if (event.type === "session.idle") await notify(event.data.session_id);
  res.sendStatus(200);
});
```

Deliveries retry with backoff on non-2xx responses or timeouts. An endpoint that keeps failing is automatically disabled (`enabled: false`) after repeated consecutive failures, so a dead URL doesn't retry forever — re-enable it once it's healthy.

### Rotating the secret

Roll a webhook secret without a delivery gap. Rotation returns a **new** secret (shown once) and keeps the **old** one valid for an overlap window, during which Managed Agents signs each delivery with **both** secrets. Update your endpoint to the new secret any time inside the window; once it expires, only the new secret signs.

```bash CLI theme={"system"}
vetta webhook rotate <webhook_id> --overlap-hours 24   # new secret; old valid up to 24h
```

Because the signature header carries both `v1=` signatures during the overlap, a verifier that accepts any matching signature keeps working across the switch with no downtime.

### Delivery log & redelivery

Every attempt is recorded, so you can see what was sent, whether it landed, and replay it if your endpoint was down.

```bash CLI theme={"system"}
vetta webhook deliveries <webhook_id>                   # recent attempts + status
vetta webhook redeliver <webhook_id> <delivery_id>      # send it again
vetta webhook update <webhook_id> --disable             # pause delivery
```

Each delivery record carries its `event_id`, `event_type`, `status` (`pending`, `succeeded`, or `failed`), the attempt count, the response status your endpoint returned, and the next retry time — enough to reconcile exactly which events reached you.

## Inbound webhooks

When you provision an [identity](https://vetta.sh/docs/identity/overview.md), Managed Agents gives you receive endpoints that turn real-world messages into stored, agent-readable events:

> [card] Email
>
> Inbound mail to a provisioned inbox is verified (SPF/DKIM/DMARC), matched, and stored.

> [card] SMS
>
> Inbound texts to a provisioned number are verified and stored.

> [card] Connections
>
> Connection lifecycle events (e.g. token expiry) are handled and reconciled.

A stored email or text that no guard holds starts one ordinary session for the agent that owns the inbox or number, run as that persona and billed to your credits — see [Inbound events](https://vetta.sh/docs/identity/inbound.md) for what is guarded and what it costs. Connection events do not wake an agent.

> [card] [Next: organizations & billing](https://vetta.sh/docs/platform/organizations.md)
>
> How accounts, teams, and money work.


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