> Every Sentinel doc, as Markdown: https://runsentinel.app/llms.txt

# Send Stacks contract alerts to Slack or a signed webhook

Route a watch's alerts to a Slack channel or to your own HTTP endpoint, signed so your handler can trust them. Two destinations, one `--to` on the watch's line.

The examples use `example-vault`; swap in your own contract.

## Slack

Connect the Sentinel Slack app to your workspace once, then name a channel.

```sh
$ sentinel connect slack
$ sentinel destinations add slack '#oncall' --default
$ sentinel when call example-vault set-paused --to slack:#oncall
```

`connect slack` prints a link that adds the app (it works once, for 10 minutes). After you allow the app, a page names your Sentinel account and asks you to confirm: nothing is connected until you press Connect. `--default` sends every watch that names no `--to` to this channel.

A private channel needs `/invite @Sentinel`, then its channel id (`#C0123ABCD`) instead of its name. An alert posted by the app keeps **Mute 1 h** and **Mark read** as buttons. Anyone in the channel can press them. A pasted Slack incoming-webhook URL works too: `sentinel destinations add slack https://hooks.slack.com/services/… --label "#oncall"`.

## A signed webhook

```sh
$ sentinel destinations add webhook https://example.com/hook --label ops
$ sentinel when call example-vault set-paused --to webhook:ops
```

The signing secret is printed once, when you add the destination. Store it as `SENTINEL_WEBHOOK_SECRET`. Each destination has its own.

Over MCP, `destinations_add` takes the same arguments: `destinations_add { kind: "webhook", target: "https://example.com/hook", label: "ops" }`.

## The body

Every webhook body is one envelope. `type` says what `data` is, and `id` is also the `webhook-id` header.

```json
{
  "id": "evt_alr_…",
  "type": "alert.created",
  "version": 1,
  "createdAt": "2026-10-09T14:02:11.000Z",
  "test": false,
  "data": { "id": "alr_…", "alertLevel": "urgent", "headline": "…", "facts": [], "signals": [] }
}
```

`data` is the alert: its headline, typed `facts`, the `signals` that fired it, and the `inspect` commands. The [full field list](/docs/watches-and-alerts#the-webhook) is in the reference.

## Verify the signature

Each request carries `webhook-id`, `webhook-timestamp` and `webhook-signature`. `verifyWebhook` from `@secondlayer/sentinel` checks them, refuses a timestamp more than 5 minutes off, and returns the event. Pass the raw body, before any JSON parsing.

```ts
import { verifyWebhook } from "@secondlayer/sentinel";

export async function POST(req: Request) {
  const event = await verifyWebhook(await req.text(), req.headers, process.env.SENTINEL_WEBHOOK_SECRET!);
  if (await seen(event.id)) return new Response(null, { status: 204 }); // a retry
  if (event.type === "alert.created" && event.data.alertLevel === "urgent") await page(event.data);
  return new Response(null, { status: 204 });
}
```

Without the package, the signature is the Standard Webhooks scheme: `v1,` then the base64 HMAC-SHA256 of `webhook-id.webhook-timestamp.body`. Details are in [Verify the signature](/docs/watches-and-alerts#verify-the-signature).

## Check it before a real alert

```sh
$ sentinel test-alert example-vault --yes
```

This sends the newest real match of the contract's watches from the last 90 days, marked as a test with its date. The webhook body carries `test: true`.

## What to know

| | |
|---|---|
| After the block | An alert arrives after the block that holds the event, never from the mempool. The measured delay is public: `sentinel status`. |
| Retries | A failed delivery is retried with backoff, up to 5 attempts. A retry reuses the same `webhook-id` and is signed again with a fresh timestamp, so dedupe on `webhook-id` only. |
| Failed endpoint | If every attempt fails, you get an email and your other webhooks get `delivery.failed`. Fix the endpoint, then run `sentinel redeliver <alert>`. |
| Slack buttons | Anyone in the channel can press Mute and Mark read. |
| Cost | Alerts are never charged. Live mainnet alerts start at Builder, $19 per contract a month ([Pricing](/pricing)). |

## Related

- [Watches & alerts: where alerts go](/docs/watches-and-alerts#where-alerts-go)
- [Monitor a Stacks contract and get alerts](/docs/monitor-a-stacks-contract)
- [sBTC and PoX-5 alerts](/docs/sbtc-and-pox-5-alerts)
