Monocle notifies your team by email, in the app, and through notification channels: Slack, Discord, and your own webhooks.
What triggers a notification
| Event | Who is notified | Where |
|---|---|---|
| Uptime monitor down or recovered | Members with access to the project | Email, in-app, channels of the monitor |
| Endpoint alert fired or resolved | Members with access to the project, if the alert notifies App members | Email, in-app, channels of the alert |
| New or regressed exceptions | Members with access to the project | Email, channels of the matching rule |
| Issue assigned to you | The new assignees | Email, in-app |
| Status of an issue changed | The assignees of the issue | Email, in-app |
| New comment on an issue | The assignees of the issue, except the author of the comment | Email, in-app |
| Weekly recap | Members with access to the project | Email, in-app |
| Reply to your feedback, or status change | You, as the author of the feedback | Email, in-app |
| Usage at 80% and 100% of the plan | All members of the organization | |
| Trial ending soon, or ended | Organization admins |
In-app notifications
The bell in the sidebar footer, above Settings, lists your latest notifications grouped by Today, Yesterday, and Earlier. Its badge counts the notifications you haven't seen yet, and clears when you open the bell.
Click a notification to open what it is about and mark it as read:
| Notification | Opens |
|---|---|
| Issue assigned, status changed, comment | The issue |
| Uptime monitor down or recovered | The page of the monitor |
| Endpoint alert fired or resolved | The page of the alert |
| Weekly recap | The recaps of the project |
| Reply to your feedback, status change | The feedback conversation |
Hover a notification to mark it as read without opening it. Mark all read clears the list. Open inbox, or the i key, opens every notification, with filters on unread notifications and on categories. In the inbox, j and k move between notifications, Enter opens one, and e marks it as read.
Notification channels
A notification channel is a Slack, Discord, or webhook destination that you add once and reuse. Uptime monitors, endpoint alerts, and exception rules all pick from the same list of channels.
Organization admins manage channels in Settings → Integrations, under Notification channels. Every member can pick them in their monitors, alerts, and exception rules.
To add a channel:
- Click Add channel.
- Choose the Type: Slack, Discord, or Webhook. The type can't be changed later.
- Give the channel a Name, such as
#prod-alerts. - Paste the Webhook URL. For Slack, create an Incoming Webhook. For Discord, create a webhook in the settings of your Discord channel, under Integrations. For a webhook channel, see Webhook channels.
- Optionally, make it a default channel. See Default channels.
- Click Send test to check that the message arrives, then Add channel.
Each channel lists what uses it, such as Used by 2 alerts · 3 monitors · 1 exception route. Monocle never shows the URL of a channel again. To change it, edit the channel and paste a new URL. Every monitor, alert, and rule that uses the channel follows the change.
When you delete a channel, the monitors and alerts that used it stop notifying it. An alert left without destination keeps being evaluated. The exception rules that posted to the channel are removed, so the next matching rule applies.
Default channels
Under Use by default, a channel can be a default for:
- Uptime monitors: every monitor that keeps the Default channels switch on.
- Exceptions: every project without an exception rule that matches. See Exception rules.
With Only for production environments, the channel is a default only for production environments. Default channels don't apply to endpoint alerts. Each alert picks its own channels.
Exception rules
New and regressed exceptions go to the channels that match their service and environment. Each project has its own rules. Open Settings → Projects, open the project, and find the Exception alerts section. You need project manager access to change them.
Click Add rule and fill in a row:
| Column | Values |
|---|---|
| Service | Any service, or one service |
| Environment | Any environment, or one environment |
| Posts to | A Slack, Discord, or webhook channel. Or Mute Slack, Mute Discord, or Mute webhooks to stop posting there. |
The type of the channel decides where the rule applies. Monocle resolves Slack, Discord, and webhooks separately. For each type, the most specific matching rule wins:
- A rule for the service and the environment.
- A rule for the service.
- A rule for the environment.
- A rule for any service and any environment.
A mute only stops its own type. Mute Slack for staging silences Slack for that environment, and Discord keeps posting.
The last row, Everything else, shows the organization's default channels for exceptions. A type with no matching rule posts there. If the row says Not posted, exceptions without a rule are not sent to any channel.
Exception summaries
Instead of one message per error, Monocle groups new and regressed exceptions into summaries.
- Email: Monocle collects the issues for 60 seconds, then sends one email with up to 30 issues. After an email, the next one waits at least 5 minutes.
- Channels: Monocle collects the issues for 60 seconds per channel, then posts one message with up to 10 open issues and the number of other open issues.
Weekly recaps
Every Monday, members receive a recap of each production environment for the past week: requests, error rate, average response time, top exceptions, and slowest endpoints.
Choose which emails you receive
In Settings → Notifications, the Email section has one switch per type of email:
| Switch | Emails |
|---|---|
| Uptime alerts | A monitor goes down or recovers |
| Endpoint alerts | An alert that notifies app members fires or resolves |
| Weekly recaps | The weekly summary of your projects |
| Exception summaries | The batched emails about new and regressed exceptions, for the current organization only |
All switches are on by default. They only change emails. In-app notifications and channels are not affected. Issue, feedback, usage, and trial emails are always sent.
Webhook channels
A webhook channel sends each event as a JSON POST to your own endpoint, signed following the Standard Webhooks specification.
The URL must:
- Use HTTPS, with no credentials and no
#fragment. - Resolve to public IP addresses only. Monocle checks the addresses when you save the channel and again before each delivery.
Monocle doesn't follow redirects and waits up to 10 seconds for an answer. Any 2xx status is a success. Monocle retries exception summaries after a timeout, a network error, or a 408, 429, or 5xx status, with a growing delay of up to one hour. It sends other events once, without retries.
Requests
Each request has these headers:
| Header | Value |
|---|---|
content-type | application/json |
user-agent | Monocle-Webhooks/1.0 |
webhook-id | The ID of the message. Retries of an exception summary keep the same ID. |
webhook-timestamp | When the request was sent, in Unix seconds |
webhook-signature | v1, followed by the signature, in base64 |
The body always has the same envelope:
{
"type": "alert.firing",
"timestamp": "2026-10-17T10:00:05.123Z",
"data": {}
}Verify the signature
When you add a webhook channel, Monocle creates its signing secret, which starts with whsec_. Organization admins can show it with Reveal secret, or replace it with Rotate secret. Rotating takes effect at once, so update your receiver right after.
The signature is an HMAC-SHA256 of <webhook-id>.<webhook-timestamp>.<body>, keyed with the base64-decoded part of the secret after whsec_. The standardwebhooks package checks it for you, along with the age of the timestamp:
import { Webhook } from 'standardwebhooks'
const webhook = new Webhook(process.env.MONOCLE_WEBHOOK_SECRET!)
export async function handle(request: Request) {
const body = await request.text()
// Throws if the signature or the timestamp is invalid
const event = webhook.verify(body, {
'webhook-id': request.headers.get('webhook-id')!,
'webhook-timestamp': request.headers.get('webhook-timestamp')!,
'webhook-signature': request.headers.get('webhook-signature')!,
})
return new Response(null, { status: 204 })
}Verify the raw body, before parsing it. Parsing and serializing the JSON again can change its bytes and break the signature.
Send test on a saved channel signs with its secret. Send test in the form, before you add the channel, signs with a temporary secret, so your receiver can't verify that one.
Webhook payloads
Dates are ISO 8601 strings.
uptime.down
{
"type": "uptime.down",
"timestamp": "2026-10-17T10:00:05.123Z",
"data": {
"monitorId": "0199f2a4-6c1e-7d3a-9b2f-3c4d5e6f7a8b",
"monitorName": "API health",
"monitorUrl": "https://api.example.com/health",
"errorType": "timeout",
"environmentName": "Production",
"applicationName": "Shop",
"organizationSlug": "acme",
"url": "https://monocle.sh/acme/shop/production/uptime/0199f2a4-6c1e-7d3a-9b2f-3c4d5e6f7a8b",
"downAt": "2026-10-17T10:00:00.000Z"
}
}errorType is one of timeout, dns, connection, tls, http, ssrf, or unknown.
uptime.recovered
{
"type": "uptime.recovered",
"timestamp": "2026-10-17T10:12:05.123Z",
"data": {
"monitorId": "0199f2a4-6c1e-7d3a-9b2f-3c4d5e6f7a8b",
"monitorName": "API health",
"monitorUrl": "https://api.example.com/health",
"environmentName": "Production",
"applicationName": "Shop",
"organizationSlug": "acme",
"url": "https://monocle.sh/acme/shop/production/uptime/0199f2a4-6c1e-7d3a-9b2f-3c4d5e6f7a8b",
"recoveredAt": "2026-10-17T10:12:00.000Z",
"downAt": "2026-10-17T10:00:00.000Z"
}
}downAt can be null.
alert.firing
{
"type": "alert.firing",
"timestamp": "2026-10-17T10:00:05.123Z",
"data": {
"ruleId": "0199f2a4-7e2d-7c11-8a3b-4d5e6f7a8b9c",
"ruleName": "p95 latency · POST /api/checkout > 800ms",
"metric": "p95",
"comparator": "gt",
"threshold": 800,
"windowMinutes": 5,
"method": "POST",
"route": "/api/checkout",
"serviceName": "api",
"applicationName": "Shop",
"environmentName": "Production",
"organizationSlug": "acme",
"alertUrl": "https://monocle.sh/acme/shop/production/alerts/0199f2a4-7e2d-7c11-8a3b-4d5e6f7a8b9c",
"endpointUrl": "https://monocle.sh/acme/shop/production/endpoints/...",
"slowestTracesUrl": "https://monocle.sh/acme/shop/production/endpoints/...",
"startedAt": "2026-10-17T09:58:00.000Z",
"value": 912.4,
"samples": 1530
}
}| Field | Description |
|---|---|
metric | p50, p95, p99, avg, error_rate, server_error_rate, or request_rate |
comparator | gt for above, lt for below |
threshold, value | In milliseconds for latencies, in percent for error rates, in requests per minute for the request rate |
value | The value that fired the alert. Can be null. |
samples | The number of requests in the window |
serviceName | Can be null for an alert created before alerts required a service |
endpointUrl, slowestTracesUrl | The endpoint, and its slowest traces, around the time the alert fired |
alert.resolved
The same fields as alert.firing, without value and samples, plus:
{
"resolvedAt": "2026-10-17T10:20:00.000Z",
"peakValue": 1040.2,
"reason": "recovered"
}peakValue is the worst value of the incident, or null. reason is one of:
recovered: the metric is back within the threshold.no_data: the window had fewer requests than the minimum.rule_edited: the condition of the alert changed.rule_paused: the alert was paused.
exception.issues
{
"type": "exception.issues",
"timestamp": "2026-10-17T10:01:05.123Z",
"data": {
"total": 14,
"organizationName": "Acme",
"issues": [
{
"title": "PaymentTimeoutError: Payment provider timed out",
"application": "Shop",
"environment": "Production",
"firstAppearance": true,
"regression": false,
"url": "https://monocle.sh/acme/shop/production/issues/0199f2a4-8f3e-7a22-9c4d-5e6f7a8b9c0d"
}
]
}
}| Field | Description |
|---|---|
total | The number of open issues in the summary |
issues | Up to 10 of them, open issues only |
issues[].environment | The name of the first environment, followed by +N when the issue happened in N more |
issues[].firstAppearance | The issue is new |
issues[].regression | The issue was resolved and happened again |
test
{
"type": "test",
"timestamp": "2026-10-17T10:00:05.123Z",
"data": {
"organizationName": "Acme"
}
}