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

# Alert Channels

> Configure notification channels for issue alerts — Email, Slack, Discord, and Webhooks.

Alert channels decide *where* NightOwl tells you something needs attention. They're configured per app in **Settings → Alert Channels**, and every enabled channel fires for the same set of triggers — so you can mix a noisy Slack firehose with a quiet email summary without re-wiring your issue workflow.

## What fires an alert

Alerts are issue-lifecycle events, not per-occurrence:

| Event            | When it fires                                                                                                                                                                                                                                                                         |
| ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `issue.new`      | A brand-new fingerprint is seen for the first time. Emitted directly from the agent's drain worker.                                                                                                                                                                                   |
| `issue.reopened` | A previously resolved issue's fingerprint recurs (auto-reopen by the agent), **or** a user manually reopens an ignored/resolved issue from the dashboard, MCP, or API. The agent flips `status` back to `open` and appends a `status_changed` activity row with `actor_type='agent'`. |
| `issue.resolved` | A user resolves the issue from the dashboard, MCP, or API.                                                                                                                                                                                                                            |
| `issue.ignored`  | A user ignores the issue from the dashboard, MCP, or API.                                                                                                                                                                                                                             |

Individual exceptions *within* an already-open or already-ignored issue bump the occurrence count silently — they don't re-alert. This keeps incident storms from flooding every channel, and keeps "ignored" meaningful (ignoring an issue permanently silences future occurrences, not just the one in front of you). `resolved` is different: a recurrence is treated as a regression, so the agent auto-flips the status back to `open` and fires `issue.reopened`. Use `NIGHTOWL_REOPEN_COOLDOWN_HOURS` (see [Configuration → Issue lifecycle](/agent/configuration#issue-lifecycle)) to require N hours of silence before the auto-reopen fires — useful for flapping issues.

## The four channel types

<CardGroup cols={2}>
  <Card title="Email (BYOD SMTP)" icon="envelope">
    Delivered through your own SMTP credentials — SendGrid, Postmark, AWS SES, Mailgun, or a self-hosted MTA. Credentials are encrypted at rest with Laravel's `Crypt` facade.
  </Card>

  <Card title="Slack" icon="slack">
    Incoming webhook URL from a Slack app. Rich message formatting with issue title, environment, stack-trace excerpt, and a direct link back to the dashboard.
  </Card>

  <Card title="Discord" icon="discord">
    Channel webhook URL from Discord's **Integrations** settings. Same rich embed as Slack, adapted to Discord's format.
  </Card>

  <Card title="Webhook" icon="webhook">
    Arbitrary HTTPS endpoint. JSON payload, optional HMAC-SHA256 signature in the `X-NightOwl-Signature` header for verification.
  </Card>
</CardGroup>

You can configure any number of channels per app, and any mix of types. Each channel has an independent enabled/disabled toggle.

## Email

Email is BYOD — *bring your own deliverability*. NightOwl never sends from a shared pool, which means alerts land in your team's inbox with your domain and your reputation.

Required fields: SMTP host, port, username, password, encryption (TLS/SSL/none), from address, from name. The password is encrypted in the database via `Crypt` and decrypted only in-memory when dispatching.

Recipients must be existing team members of the app — the dashboard rejects addresses that aren't in the team. If you need to notify an external address (an on-call alias, a PagerDuty email trigger, a Slack email relay), add that address as an invited team member first, or route through a Slack/Discord/webhook channel instead.

## Slack

1. In Slack, create an app at [api.slack.com/apps](https://api.slack.com/apps) → *Incoming Webhooks* → *Add New Webhook to Workspace*.
2. Copy the webhook URL (starts with `https://hooks.slack.com/services/…`).
3. Paste into the Slack channel config in the NightOwl dashboard.

The alert includes issue title, environment badge, first five app-frame stack lines, and a **View in NightOwl** button linking to the issue detail page.

## Discord

1. In Discord, open **Server Settings → Integrations → Webhooks → New Webhook**, pick the destination channel, and copy the URL.
2. Paste into the Discord channel config.

Discord supports embeds, so the alert renders with a colored side bar (red for exceptions, amber for performance), title, and fields for environment, count, and first-seen.

## Webhook

For anything more custom — routing to PagerDuty, OpsGenie, a bespoke router, or your own incident bot — use a webhook channel. Every request is `POST application/json` with this shape:

```json theme={null}
{
  "event": "issue.new",
  "app": "shop-api",
  "app_id": "2a3b...",
  "view_url": "https://app.usenightowl.com/dashboard/2a3b.../issues/4812",
  "issue": {
    "issue_id": 4812,
    "type": "exception",
    "title": "TypeError: Cannot read properties of null",
    "message": "Cannot read properties of null (reading 'id')",
    "status": "open",
    "priority": null,
    "first_seen_at": "2026-04-14 09:12:33",
    "last_seen_at": "2026-04-21 12:45:02",
    "occurrences": 18,
    "users": 3,
    "handled": false,
    "environment": "production",
    "location": "app/Http/Controllers/OrderController.php:142",
    "php_version": "8.3.4",
    "laravel_version": "12.4.0",
    "subtype": null,
    "route": null,
    "action": null,
    "threshold_ms": null,
    "duration_ms": null
  },
  "timestamp": "2026-04-21T12:45:02+00:00"
}
```

Every key is always present; unused fields are `null`. Notes:

* `app_id` and `view_url` come from the agent's `NIGHTOWL_APP_ID` env var. When set, both fields embed the connected-app ID and a direct link to the issue page. When unset, `app_id` is `null` and `view_url` falls back to the generic `/dashboard` root. See [agent configuration](/agent/configuration).
* `message` is truncated to 200 characters with an ellipsis.
* **Exception events** populate `handled`, `location`, `php_version`, `laravel_version`. **Performance events** populate `route`, `threshold_ms`, `duration_ms`, and use `subtype` to distinguish `route` / `job` / `command` / `query` / etc. The unused set is `null`.
* `status` and `priority` reflect the row's current dashboard state at dispatch time. For a brand-new fingerprint, `status` is `"open"` (the DB default) and `priority` is `null` until a user assigns one. When a previously-resolved issue recurs, the agent flips `status` back to `"open"` before dispatching `issue.reopened`, so receivers see the new state immediately.
* Field `type` is always `"exception"` or `"performance"`.

### Signing

If you set a **signing secret** on the channel, NightOwl signs each request with HMAC-SHA256 over the raw JSON body:

```
X-NightOwl-Signature: <hex-digest>
```

The header value is the raw hex digest — **no `sha256=` prefix**. Verify by recomputing the digest with your secret and the raw body (before any JSON parsing), then comparing with `hash_equals`. Reject any request whose signature doesn't match.

## Testing a channel

Every channel has a **Send test** button. The test payload is deliberately minimal — just enough to prove the transport works — so don't expect it to match the real issue schema:

* **Webhook**: `{ "event": "test", "app": "<app name>", "timestamp": "<iso8601>" }`. No `issue` object.
* **Slack / Discord**: a plain "Test Notification" message in the destination channel.
* **Email**: a short HTML email with the subject `[<app-name>] NightOwl Test Notification`.

All tests use the same credentials and transport as real alerts, so a successful test guarantees real alerts will reach the same destination. If a test fails, the dashboard surfaces the specific error (SMTP auth failure, 4xx response from a webhook, invalid Slack URL) rather than a generic message.

## Toggling and disabling

Channels can be disabled without deleting. A disabled channel is skipped by the dispatcher but preserves its config and history — useful for silencing a channel during maintenance without losing the webhook URL or SMTP credentials.

## Under the hood

Issue alerts come from two places:

* **`issue.new` and `issue.reopened` (auto-reopen)** — the NightOwl agent's drain worker dispatches directly from the customer app: `issue.new` after a brand-new fingerprint is upserted, `issue.reopened` after a `resolved` fingerprint recurs (subject to `NIGHTOWL_REOPEN_COOLDOWN_HOURS`). No round-trip through NightOwl's API.
* **`issue.resolved` / `issue.ignored` / `issue.reopened` (manual)** — the NightOwl API dispatches via a queued `DispatchIssueAlertsJob` whenever a user changes status from the dashboard, MCP, or the API. `issue.reopened` therefore has two emission paths (agent auto-reopen and user-initiated reopen), but the payload shape is identical.

Both paths emit the same payload shape, share `X-NightOwl-Signature` signing, and respect per-channel `notify_events` filtering. Dispatch is isolated per channel with a short timeout — one broken webhook never blocks Slack or email from firing.
