# Alert delivery and Slack routing

> How Voice alert rules are evaluated centrally and delivered to your org's Slack workspace, and how that differs from webhook event delivery.

Alert *rules* for Voice are not something you author per org. FirstByte
evaluates them centrally in Grafana against the platform's own telemetry. The
only tenant-facing knob is **delivery**: where this org's Voice alerts get
posted. You supply a Slack incoming-webhook URL on the **Alerts** screen of the
Contact Center or Voice AI console, and the
control-plane API creates a per-org Grafana contact point named
`cc-<org>-voice` that the Voice alert rules route to.

That means there is exactly one delivery target per org per vertical. The
**Alerts** screen in the Contact Center and Voice AI consoles
(**Analytics → Alerts**) is pinned to the `voice` vertical.

## What the platform evaluates for you

The alert rules live in Grafana, on the platform side. You do not create,
edit, or list them from the console or from the tRPC API. When you save a
Slack webhook, the control-plane API creates the contact point for your org.
It then creates the Voice alert rules in a Grafana folder for your org. The
rules route to that contact point and carry the label `tenant=<org>`. The
success message on save reports both halves of that work:

| Response field     | Meaning                                                                    |
| ------------------ | -------------------------------------------------------------------------- |
| `provisioned`      | The contact point `cc-<org>-voice` was created or updated in Grafana.      |
| `rulesProvisioned` | How many alert rules were created to route to that contact point.          |

New rules are created **paused**. An operator reviews them and enables them.
Until an operator enables a rule, it does not fire for your org, even when the
save reported it as provisioned.

If `provisioned` comes back false, the console reports
*"Saved · webhook stored."* The URL is stored against your org, but the
contact point was not created on this attempt. Nothing is delivered yet.
Save again to retry the provisioning step.

Because evaluation is central, the signals behind these alerts are the same
ones described in [Telemetry](/platform/telemetry) and
[Telephony Metrics](/glossary/metrics). Use those pages when you want to
reproduce or investigate an alert in your own dashboards.

## Configure Slack delivery for a vertical

1. In Slack, create an incoming webhook (**Apps → Incoming Webhooks**) bound
   to the channel you want alerts in. Slack's own
   [webhook documentation](https://api.slack.com/messaging/webhooks) covers
   app install and scopes.
2. Open **Alerts** in the Contact Center or Voice AI console
   (**Analytics → Alerts**) with the target org selected.
3. Paste the webhook URL into **Slack incoming-webhook URL**. The field is
   required and must be `https`. The console shows the validation error when
   you press **Save**, not before.
4. Optionally set a **Channel label** such as `#ops-alerts`.
5. Press **Save**.

Under the hood this is a single upsert, keyed on org plus vertical:

```ts
await alertChannels.upsert.mutate({
  orgId,
  vertical: "voice",
  slackWebhookUrl: "https://hooks.slack.com/services/T000/B000/xxxx",
  slackChannel: "#ops-alerts", // or null
});
```

Saving again with a different URL replaces the stored value for that
org/vertical pair — there is no list of multiple destinations. **Save** stays
disabled until the form differs from the stored record.

The current configuration is read back with
`alertChannels.get.query({ orgId, vertical: "voice" })`, which returns the
stored record (`slack_webhook_url`, `slack_channel`) or nothing when routing
has never been configured. The console's **Configured** badge is exactly this
distinction.

### Channel label is metadata

The **Channel label** field does not choose the Slack channel. A Slack
incoming webhook is already bound to one channel at creation time, and that
binding decides where messages land. The label is stored alongside the URL so
that the console can show which channel an org expects its alerts in. To move
alerts to a different Slack channel, create a new webhook in Slack for that
channel and save its URL here.

### Treat the webhook URL as a credential

Anyone holding the URL can post into your channel. It is stored against your
org and copied into the Grafana contact point. To invalidate it,
delete the webhook in Slack; removing it here stops FirstByte from using
it, but only Slack can revoke the URL itself.

## What a delivered alert looks like

Delivery is a plain Slack incoming-webhook POST from the Grafana contact
point, so alerts appear as messages in the channel the webhook is bound to,
posted by the Slack app that owns the webhook. The content is whatever the
firing platform rule emits — rule name and the notification body configured on
the rule — not a FirstByte-specific envelope you can template from the
console.

Two consequences worth planning around:

- Messages are **per firing rule for your org**, not per call. There is no
  `call_sid`-level fan-out here; for per-call data use CDRs or webhooks.
- Formatting is controlled centrally. If you need a machine-readable feed,
  consume the streams in [Telemetry](/platform/telemetry) rather than parsing
  Slack messages.

## Testing and troubleshooting delivery

Once a webhook is saved, **Send test** posts a test message straight to the
stored URL:

```ts
const r = await alertChannels.test.mutate({ orgId, vertical: "voice" });
// r.ok === true  -> Slack accepted the post
// r.ok === false -> r.status / r.message carry Slack's rejection
```

The button is disabled until a configuration exists, because the test uses the
**stored** webhook — not the text currently in the input. Save first, then
test.

| Symptom | What it means | What to do |
| ------- | ------------- | ---------- |
| *"Test message sent — check your Slack channel."* but nothing appears | Slack accepted the POST; the webhook is bound to a channel you are not looking at. | Check the channel the webhook was created for, then update the channel label so the console matches reality. |
| *"Slack rejected the test: …"* with a status or message | Slack refused the POST — typically a deleted, revoked, or mistyped webhook. | Recreate the webhook in Slack and save the new URL. |
| Save reports *"webhook stored"* with no contact point provisioned | The URL persisted but the Grafana contact point was not created on that attempt. | Press **Save** again. Until provisioning succeeds, no rules route to your contact point. |
| Test succeeds, real alerts never arrive | Delivery works. No rule has fired for your org, the rules are still paused, or provisioning of rules did not complete. | Confirm the save message reported alert rules configured. Ask an operator to confirm the rules are enabled. Then compare against the signals in [Telemetry](/platform/telemetry). |
| The URL is rejected on save | The field is required and must use `https`. | Paste the full `https://hooks.slack.com/services/...` URL. |

## Removing routing and what stops arriving

**Remove** deletes the stored routing for that org and vertical after a
confirmation prompt:

```ts
await alertChannels.remove.mutate({ orgId, vertical: "voice" });
```

After removal:

- The console clears both fields and the **Configured** badge.
- **Send test** is disabled again, because there is no stored webhook.
- Your org's Voice alerts have no Slack destination. Central evaluation
  continues — you simply stop being notified.

Removal does not touch anything in Slack. The incoming webhook keeps existing
in your workspace until you delete it there.

## Alerts vs webhooks

These are two different delivery systems and they are easy to confuse:

| | Alert delivery (this page) | [Webhooks](/platform/webhooks) |
| - | - | - |
| What is delivered | Platform alert rules firing for your org | Your application's events |
| Who defines the trigger | FirstByte, centrally in Grafana | You, when you subscribe |
| Destination | One Slack incoming webhook per org per vertical | Your HTTPS endpoint(s) |
| Granularity | Operational, org-level | Per event / per call |
| Configured with | `alertChannels.get` / `upsert` / `remove` / `test` | The webhooks procedures |

Rule of thumb: alerts go to humans in Slack so someone notices a Voice
problem; webhooks go to your backend so your code reacts to a call. Routing
alerts into Slack is not a substitute for consuming call events, and
subscribing to webhooks does not give you the platform's alert rules.

## Related

- [Telemetry](/platform/telemetry) — the metrics, traces, and CDRs the alert rules are evaluated against
- [Telephony Metrics](/glossary/metrics) — what the underlying numbers mean
- [Authentication](/concepts/authentication) — the API key your server uses to call these procedures
