> ## 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 — Vetta calls your endpoint when subscribed events occur. **Inbound** receive endpoints for email and SMS are live too: messages are verified and stored (the agent wake is coming soon — see [Inbound events](/docs/identity/inbound)).

## Outbound webhooks

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

<CodeGroup>
  ```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"],
  });
  ```
</CodeGroup>

### 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 Vetta 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](/docs/identity/overview), Vetta gives you receive endpoints that turn real-world messages into stored, agent-readable events:

<CardGroup cols={3}>
  <Card title="Email" icon="envelope">Inbound mail to a provisioned inbox is verified (SPF/DKIM/DMARC), matched, and stored.</Card>
  <Card title="SMS" icon="phone">Inbound texts to a provisioned number are verified and stored.</Card>
  <Card title="Connections" icon="link">Connection lifecycle events (e.g. token expiry) are handled and reconciled.</Card>
</CardGroup>

**Coming soon:** the wake. Today a stored inbound message does not start a session — see [Inbound events](/docs/identity/inbound) for exactly what is guarded and what is pending. When it lands, inbound events will flow through the same [durable runtime](/docs/concepts/runtime): the event is enqueued and the agent's alarm is set, so a sleeping agent wakes, handles the message in one turn, commits, and sleeps again.

<Card title="Next: organizations & billing" icon="building" href="/docs/platform/organizations">
  How accounts, teams, and money work.
</Card>
