# Slack and webhooks

Slack and webhooks send VitalSentinel alerts outside the app. A Slack channel gets each alert as a Slack message. A webhook sends each alert to your own web address as signed JSON, for on-call tools, chat bots or scripts.

## What Slack and webhooks are for [#what-slack-and-webhooks-are-for]

- **Slack channels**: your team reads the alerts in a Slack channel. Each alert message has the problem, the facts, the next steps and a button that opens the alert in VitalSentinel.
- **Webhooks**: VitalSentinel sends each alert to an endpoint. An endpoint is a web address on your server or in another tool that receives data. Use a webhook to send alerts to a tool that VitalSentinel does not connect to directly.

You connect Slack channels and webhooks once for the workspace. Then you select them in each alert rule. The in-app notification and the email notification stay available on every plan. See [Alert delivery](/domain/alerts/delivery).

## Before you start [#before-you-start]

- **Plan**: Slack and webhooks are available on Lite and higher plans. On a lower plan, the workspace **Integrations** page shows the chip **On Lite and higher plans** and the text "Available on Lite and higher plans. Email and in-app alerts stay on every plan." A workspace owner also sees **See plans**. See [Plans and limits](/workspace/plans-and-limits).
- **Role**: only workspace owners and admins can connect Slack, add webhooks and use the buttons on each row. Members see the rows and **Recent deliveries**, but no other buttons.
- **Where**: open the workspace **Settings**, then click **Integrations** in the **Workspace Settings** sidebar. The Slack and webhook cards are below **Google accounts**.

## Connect a Slack channel [#connect-a-slack-channel]

Each Slack channel is a separate connection. To send alerts to two channels, connect Slack two times.

1. On the workspace **Integrations** page, click **Add to Slack**.
2. On the Slack page, sign in to your Slack workspace if Slack asks.
3. Select the channel for your alerts.
4. Approve the request.

Slack then sends you back to the **Integrations** page. The message **Slack channel connected** confirms the connection. The new row shows the channel name, the Slack workspace and the chip **Connected**.

If no Slack channel is connected yet, the card shows **No Slack channels connected** and "Pick a channel and VitalSentinel posts your alerts there. Each channel is a separate connection."

### Connect Slack from an alert rule [#connect-slack-from-an-alert-rule]

If the workspace has no Slack channel, the **Delivery** part of an alert rule shows a **Slack** card with **Connect Slack**.

1. Click **Connect Slack**. Slack opens in a new tab.
2. In Slack, select the channel and approve the request.
3. Go back to the tab with the alert rule. The new channel shows in the rule without a reload.

### Private channels and archived channels [#private-channels-and-archived-channels]

- **Private channel**: you must be a member of a private channel before you can select it in Slack. Join the channel in Slack first, then click **Add to Slack** again.
- **Archived channel**: Slack does not accept messages in an archived channel. If somebody archives a connected channel, the row shows **Needs reconnecting** and "The channel was archived". Unarchive the channel in Slack and reconnect it, or connect another channel.

### Slack workspace admin approval [#slack-workspace-admin-approval]

Some Slack workspaces allow only apps that an owner or app manager approved. In that case, Slack does not let you install VitalSentinel at once. If your Slack workspace allows requests, you can send a request for approval in Slack. When your Slack admin approves VitalSentinel, click **Add to Slack** again. Some Slack workspaces restrict an app completely. Then only your Slack admin can change that.

## Slack channel rows [#slack-channel-rows]

Each row in the **Slack channels** card shows the channel (for example **#alerts**), the Slack workspace and a status chip:

| Chip | Meaning |
|---|---|
| **Connected** | Alerts go to the channel. The row shows **Send test**. |
| **Needs reconnecting** | Slack refused the messages. Alerts to the channel are paused. The row shows **Reconnect**. |

A row with **Needs reconnecting** gives the reason, followed by "Alerts are paused.":

- "The channel was removed"
- "The channel was archived"
- "The app was removed from the channel"
- "Slack no longer allows posting there"
- "The saved connection could not be read"
- "Slack refused our messages" (any other reason)

When a channel stops working, the person who connected it gets one notice in the app: "Slack channel #name stopped working". The notice also says: "If it stays disconnected for 14 days, we remove it."

### Send a test message to Slack [#send-a-test-message-to-slack]

1. On the row of the channel, click **Send test**.
2. Open the channel in Slack and look for the test message.

If the test works, the message "Test message sent. Check the channel in Slack." shows. If it fails, the message starts with "The test did not go through" and gives the reason in brackets, for example `HTTP 404 channel_not_found`. The Slack test message has the tag **Test** and the title "Test alert from VitalSentinel".

### Reconnect a Slack channel [#reconnect-a-slack-channel]

1. On the row with **Needs reconnecting**, click **Reconnect**.
2. In Slack, select the same channel and approve the request.

The row then shows **Connected** again, and the alert rules that used the channel use it again. If you select a different channel, VitalSentinel adds it as a new row. The old row stays until you disconnect it. If a channel stays disconnected for 14 days, VitalSentinel removes it and sends you a notice.

### Disconnect a Slack channel [#disconnect-a-slack-channel]

1. On the row of the channel, click the trash icon.
2. In the dialog **Disconnect Slack channel**, click **Disconnect**.

Alerts stop going to the channel, and VitalSentinel removes the channel from every alert rule. VitalSentinel also revokes its access in Slack and deletes the saved connection.

## Use a channel or a webhook in an alert rule [#use-a-channel-or-a-webhook-in-an-alert-rule]

1. Open an alert rule, or click **Create alert** on the **Alerts** page.
2. Under **Delivery**, select each Slack channel card (**Slack – #channel**) and webhook card (**Webhook – name**) that must get the alert.
3. Click **Save alert**.

You cannot add a channel with **Needs reconnecting** or a webhook with **Turned off** to a rule. If the workspace has no webhook, the rule shows a **Webhooks** card with **Set up a webhook**. See [Alert delivery](/domain/alerts/delivery).

## Add a webhook [#add-a-webhook]

1. On the workspace **Integrations** page, click **Add webhook**. The **Add a webhook** window opens.
2. In **Name**, enter a name, for example "Ops on-call".
3. In **Endpoint URL**, enter the address of your endpoint. It must be a public `https://` address.
4. Click **Add webhook**. The **Copy your signing secret** window opens.
5. Click the copy icon next to **Signing secret**.
6. Keep the secret in a safe place, for example in your password manager.
7. Click **I have saved it**.

With no webhook, the card shows **No webhooks yet** and "Send alerts to your own endpoint as signed JSON, for on-call tools, chat bots or scripts."

### The signing secret [#the-signing-secret]

<Callout type="warn">
  The signing secret shows only one time. VitalSentinel cannot show it again. If you lose it, rotate the secret and update your endpoint.
</Callout>

The signing secret starts with `whsec_`. Your endpoint uses it to make sure that a request came from VitalSentinel and that nobody changed it. See [Verify the webhook signature](#verify-the-webhook-signature).

## Webhook rows [#webhook-rows]

Each row in the **Webhooks** card shows the webhook name, the host of the endpoint and a status chip: **On** or **Turned off**. A turned-off row also shows the reason, for example "10 failed deliveries in a row". The row has these buttons:

- **Send test**: sends a test alert to the endpoint. This button shows only when the webhook is on.
- **Turn off** or **Turn on**: stops or starts the alerts to the endpoint.
- The key icon: rotates the signing secret.
- The trash icon: removes the webhook.

### Send a test to a webhook [#send-a-test-to-a-webhook]

Click **Send test** on the row. If your endpoint answers with a success status, the message "Test delivered. Your endpoint answered with a success status." shows. If not, the message starts with "The test did not go through" and gives the reason, for example `HTTP 401`. The test request has the `type` value `test`. A failed test does not turn the webhook off.

### Rotate the signing secret [#rotate-the-signing-secret]

<Callout type="warn">
  When you rotate the secret, the current secret stops working at once. Requests signed with the new secret fail on your endpoint until you update it.
</Callout>

1. On the row, click the key icon.
2. In the dialog **Rotate signing secret**, click **Rotate secret**.
3. In the **Copy your signing secret** window, copy the new secret.
4. Update the secret on your endpoint.
5. Click **I have saved it**.

### Remove a webhook [#remove-a-webhook]

1. On the row, click the trash icon.
2. In the dialog **Remove webhook**, click **Remove webhook**.

Alerts stop going to the endpoint, and VitalSentinel removes the webhook from every alert rule. VitalSentinel also deletes its signing secret.

## Recent deliveries [#recent-deliveries]

Click **Recent deliveries** on a Slack channel row or a webhook row to see the last ten messages sent there, newest first. Each line shows a status, **Alert** or **Test message**, the error (if there is one) and the date and time.

| Status | Meaning |
|---|---|
| **Sent** | Slack or your endpoint accepted the message. |
| **Failed** | The delivery did not work. The line shows the reason, for example `HTTP 500` or `network error`. |
| **Skipped** | VitalSentinel did not send the message, for example because the webhook is off or the channel needs reconnecting. |
| **Pending** | The delivery is not finished yet. |

If VitalSentinel tried a delivery more than one time, the line shows the attempt number, for example "attempt 2". If nothing was sent yet, the list shows "Nothing has been sent here yet."

## What VitalSentinel sends to a webhook [#what-vitalsentinel-sends-to-a-webhook]

VitalSentinel sends each alert to the endpoint as an HTTPS `POST` request with a JSON body. An alert rule sends one request when the alert starts and one when the problem stops.

### Webhook headers [#webhook-headers]

| Header | Value |
|---|---|
| `Content-Type` | `application/json` |
| `User-Agent` | `VitalSentinel-Webhooks/1` |
| `X-VitalSentinel-Event-Id` | The ID of the event. It is the same on each retry of the same event. |
| `X-VitalSentinel-Timestamp` | The time of this attempt, in Unix seconds. |
| `X-VitalSentinel-Signature` | `v1=` followed by the signature, in hexadecimal. |

### Webhook payload [#webhook-payload]

```json
{
  "schema": "vitalsentinel.alert.v1",
  "event_id": "6f1a0f6e-2b3c-4d5e-8f90-a1b2c3d4e5f6",
  "type": "fired",
  "timestamp": "2026-10-16T10:00:00+00:00",
  "rule": { "id": "…", "name": "Site is down" },
  "domain": { "id": "…", "name": "example.com" },
  "module": "uptime",
  "metric": "is_down",
  "severity": "critical",
  "title": "Site is down",
  "message": "Your site does not answer. Visitors cannot open it.",
  "metrics": {
    "current_value": 1.0,
    "threshold_value": 1.0,
    "current_display": "Yes",
    "threshold_display": "Yes",
    "device": null
  },
  "url": "https://app.vitalsentinel.com/dashboard/domains/…/uptime"
}
```

### Payload fields [#payload-fields]

| Field | Meaning |
|---|---|
| `type` | `fired` when the alert starts, `resolved` when the problem stops, `test` for **Send test**. |
| `severity` | `critical`, `warning` or `info`. |
| `title`, `message` | The headline and the sentence that the alert shows in the app. |
| `rule`, `domain` | The alert rule and the domain, each with an `id` and a `name`. |
| `metrics` | The current value and the threshold, as numbers and as display text. Values can be `null`. |
| `url` | The link that opens the alert in VitalSentinel. |
| `event_id` | The ID of the event. A `resolved` event has its own ID. |

New fields can come in a later version of `vitalsentinel.alert.v1`. Make your endpoint ignore fields that it does not know.

## Verify the webhook signature [#verify-the-webhook-signature]

Each webhook request has a signature. Your endpoint calculates the same signature with your signing secret. If the two values are the same, the request came from VitalSentinel and nobody changed it.

### How the signature is made [#how-the-signature-is-made]

1. Take the value of the `X-VitalSentinel-Timestamp` header.
2. Add a period (`.`) and the raw request body, exactly as received.
3. Calculate an HMAC-SHA256 of this text. Use the full signing secret, with `whsec_`, as the key.
4. Write the result in lowercase hexadecimal and put `v1=` before it.
5. Compare the result with the `X-VitalSentinel-Signature` header.

Also reject a request if the timestamp is more than five minutes from your server time. This stops somebody who sends an old request again. Use the raw body, not a parsed and re-encoded JSON object, because a small change makes the signature different.

### Node.js example [#nodejs-example]

```js
import crypto from 'node:crypto';

// rawBody is a Buffer with the request body, exactly as received.
export function verify(secret, rawBody, timestamp, signature, toleranceSeconds = 300) {
  if (Math.abs(Date.now() / 1000 - Number(timestamp)) > toleranceSeconds) return false;
  const expected =
    'v1=' + crypto.createHmac('sha256', secret).update(`${timestamp}.`).update(rawBody).digest('hex');
  const a = Buffer.from(expected);
  const b = Buffer.from(signature);
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}
```

In Express, use `express.raw({ type: 'application/json' })` on the webhook route to get the raw body.

### Python example [#python-example]

```python
import hashlib, hmac, time

def verify(secret: str, body: bytes, timestamp: str, signature: str, tolerance: int = 300) -> bool:
    if abs(time.time() - int(timestamp)) > tolerance:
        return False
    expected = "v1=" + hmac.new(secret.encode(), timestamp.encode() + b"." + body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, signature)
```

Both examples compare the values in constant time, so the time of the comparison gives no information about the signature.

## Retries and automatic turn-off [#retries-and-automatic-turn-off]

Your endpoint must answer with a `2xx` status code to accept a webhook request. Answer fast, in less than five seconds, and do slow work after you answer.

- **Retries**: if your endpoint answers `408`, `429` or a `5xx` status, or does not answer, VitalSentinel tries again. It makes up to four attempts in total, with a short wait between them. Each attempt has a new timestamp and a new signature.
- **No retries**: VitalSentinel does not retry other status codes, for example `400`, `401` or `404`. It also does not follow redirects, so a `3xx` answer is a failed delivery.
- **Duplicates**: a retry has the same `event_id`. Use it to ignore an event that your endpoint already processed.
- **Automatic turn-off**: if 10 deliveries in a row fail, VitalSentinel turns the webhook off. The row shows **Turned off** and "10 failed deliveries in a row". The person who added the webhook gets the notice "Webhook "name" was turned off".

To start the alerts again, fix the endpoint, click **Send test**, then click **Turn on**.

## Troubleshooting [#troubleshooting]

### Why can I not see Add to Slack or Add webhook? [#why-can-i-not-see-add-to-slack-or-add-webhook]

- If the card shows **On Lite and higher plans**, the workspace plan does not include Slack and webhooks. See [Plans and limits](/workspace/plans-and-limits).
- If you are a member, the buttons do not show. Ask a workspace owner or admin.

### Why does the Integrations page say "Slack was not connected"? [#why-does-the-integrations-page-say-slack-was-not-connected]

| Message | What to do |
|---|---|
| "You declined access in Slack, so nothing was connected." | Click **Add to Slack** again and approve the request. |
| "That connection attempt expired, so nothing was connected. Please try again." | Click **Add to Slack** again and finish the steps in Slack without a long pause. |
| "Only workspace owners and admins can connect Slack, so nothing was connected." | Ask a workspace owner or admin to connect Slack. |
| "Slack alerts are available on Lite and higher plans, so nothing was connected." | Upgrade the plan, or use email alerts. |
| "Slack could not finish connecting, so nothing was connected. Please try again." | Try again. If it fails again, contact support. |

### Why can I not install VitalSentinel in Slack? [#why-can-i-not-install-vitalsentinel-in-slack]

Your Slack workspace can require admin approval for apps. If Slack lets you send a request, send it. After your Slack admin approves VitalSentinel, click **Add to Slack** again. If you cannot find a private channel in the Slack list, join that channel in Slack first.

### Why does a Slack channel show "Needs reconnecting"? [#why-does-a-slack-channel-show-needs-reconnecting]

Slack refused the alerts for the channel, and the row gives the reason. For example, somebody archived or deleted the channel, or removed the VitalSentinel app. If the channel still exists, click **Reconnect** and select the same channel. If the channel is gone, disconnect the row and connect another channel.

### Why is my endpoint URL refused? [#why-is-my-endpoint-url-refused]

The **Endpoint URL** field shows the reason:

- "The URL must start with https://": use an `https://` address. VitalSentinel does not send to `http://`.
- "That address is not reachable from the internet. Use a public HTTPS URL.": the address points to a private network, for example `localhost` or an internal server. Use a public address.
- "Enter a valid HTTPS URL without a username or password.": remove the user name and password from the address.

### Why do my webhook deliveries fail? [#why-do-my-webhook-deliveries-fail]

Open **Recent deliveries** on the webhook row and read the reason.

- `HTTP 401` or `HTTP 403`: your endpoint or a firewall refuses the request. Allow requests without a login, and check the signature instead.
- `HTTP 404`: the path of the endpoint is wrong.
- `HTTP 301` or `HTTP 302`: your endpoint redirects. Enter the final address.
- `network error`: your endpoint did not answer in time, or the TLS certificate is not valid.

### Why does the webhook signature not match? [#why-does-the-webhook-signature-not-match]

- Calculate the signature on the raw request body. A JSON parser changes the spaces and the order of the text.
- Use the full secret, with `whsec_` at the start.
- If somebody rotated the secret, use the new secret.
- Use the timestamp from the `X-VitalSentinel-Timestamp` header of the same request.

### Why was my webhook turned off? [#why-was-my-webhook-turned-off]

VitalSentinel turns a webhook off after 10 failed deliveries in a row. Fix the endpoint, click **Send test**, and if the test works, click **Turn on**.

### Why do some deliveries show "Skipped"? [#why-do-some-deliveries-show-skipped]

VitalSentinel skips a delivery in these cases: the webhook is off, the Slack channel needs reconnecting, or the plan no longer includes Slack and webhooks. The line shows the reason. Fix the cause, and the next alerts go out again.

## Related [#related]

- [Alert delivery](/domain/alerts/delivery) - choose the channels of an alert rule and read alert messages
- [Alerts](/domain/alerts) - create alert rules
- [Integrations](/workspace/integrations) - the workspace page with the Slack and webhook cards
- [Plans and limits](/workspace/plans-and-limits) - which plans include Slack and webhooks
