Skip to main content

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 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:
EventWhen it fires
issue.newA brand-new fingerprint is seen for the first time. Emitted directly from the agent’s drain worker.
issue.reopenedA 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.resolvedA user resolves the issue from the dashboard, MCP, or API.
issue.ignoredA 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) to require N hours of silence before the auto-reopen fires — useful for flapping issues.

The four channel types

Email (BYOD SMTP)

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.

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.

Discord

Channel webhook URL from Discord’s Integrations settings. Same rich embed as Slack, adapted to Discord’s format.

Webhook

Arbitrary HTTPS endpoint. JSON payload, optional HMAC-SHA256 signature in the X-NightOwl-Signature header for verification.
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/appsIncoming WebhooksAdd 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:
{
  "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.
  • 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.