Source: https://cli.nylas.com/guides/prometheus-alertmanager-email

# Email Prometheus Alerts without SMTP

Route Prometheus Alertmanager alerts to email by pointing a webhook receiver at a handler that runs nylas email send, instead of configuring Alertmanager's built-in SMTP relay.

Written by [Nick Barraclough](https://cli.nylas.com/authors/nick-barraclough) Product Manager

Reviewed by [Qasim Muhammad](https://cli.nylas.com/authors/qasim-muhammad)

Updated June 9, 2026

> **TL;DR:** Add a `webhook_config` receiver in Alertmanager that POSTs to a tiny HTTP handler. The handler parses the alert JSON and calls [`nylas email send`](https://cli.nylas.com/docs/commands/email-send). No `smtp_smarthost`, no app password. The payoff: one moving part to rotate when credentials change, shown at the end.

## Why skip Alertmanager's built-in SMTP?

Alertmanager ships an `email_config` receiver, but it needs a working SMTP relay: a host, port 587, a username, and a password stored in the config file. Google disabled less-secure-app SMTP access in May 2022 and Microsoft 365 turned off basic SMTP auth, so that password is usually an app password or an OAuth relay you maintain separately. The Nylas CLI moves email out of Alertmanager entirely.

With a webhook receiver, Alertmanager only speaks HTTP to a local handler, and the handler owns delivery. According to the Alertmanager [receiver settings docs](https://prometheus.io/docs/alerting/latest/configuration/#receiver-integration-settings), a `webhook_config` takes a single `url` and an optional bearer token. That is the whole contract. You stop maintaining a `smtp_smarthost`, a TLS config block, and a secret that 2 systems both depend on. The CLI authenticates with one API key and refreshes OAuth tokens on its own.

## How do I configure the Alertmanager webhook receiver?

An Alertmanager webhook receiver is a route target that POSTs alert groups as JSON to a URL you control. You define it under `receivers` with a `webhook_config` block, then point a `route` at it. Alertmanager batches alerts and retries failed POSTs, so your handler receives grouped, deduplicated events rather than one request per firing rule.

The config below sends every alert to a handler listening on `localhost:9095`. Alertmanager groups alerts by `alertname` and waits `group_wait` (30 seconds by default) before the first POST, which means a burst of 50 related alerts arrives as 1 batched payload. Keep `send_resolved: true` so the handler can email both firing and resolved states.

```yaml
route:
  receiver: nylas-email
  group_by: ['alertname']
  group_wait: 30s
  group_interval: 5m
  repeat_interval: 4h

receivers:
  - name: nylas-email
    webhook_configs:
      - url: 'http://localhost:9095/alert'
        send_resolved: true
```

Validate the file before reloading: `amtool check-config alertmanager.yml` parses the syntax, and a reload via `SIGHUP` or `POST /-/reload` applies it without restarting the process.

## What does the webhook handler look like?

The handler is a short HTTP server that decodes the Alertmanager payload and shells out to the CLI once per batch. Alertmanager's webhook payload is documented JSON: a top-level `status` field plus an `alerts` array, each alert carrying `labels` and `annotations` maps. The example uses about 30 lines of Python and the standard library only.

The `nylas email send` command sends a message across providers without any SMTP relay. It reads `NYLAS_API_KEY` from the environment, takes the grant ID as its first argument, and the `--yes` flag skips the interactive confirmation so it runs unattended. The handler builds the subject from the alert name and status, then passes the rendered summary as the body.

```python
import json, os, subprocess
from http.server import BaseHTTPRequestHandler, HTTPServer

GRANT = os.environ["NYLAS_GRANT_ID"]
TO = os.environ["ALERT_EMAIL_TO"]

class Handler(BaseHTTPRequestHandler):
    def do_POST(self):
        body = self.rfile.read(int(self.headers["Content-Length"]))
        data = json.loads(body)
        status = data.get("status", "firing")
        names = sorted({a["labels"].get("alertname", "alert") for a in data["alerts"]})
        subject = f"[{status.upper()}] {', '.join(names)}"
        lines = []
        for a in data["alerts"]:
            summary = a["annotations"].get("summary", a["labels"].get("alertname", ""))
            lines.append(f"- {a['labels'].get('severity', 'none')}: {summary}")
        subprocess.run([
            "nylas", "email", "send", GRANT,
            "--to", TO,
            "--subject", subject,
            "--body", "\n".join(lines),
            "--yes", "--json",
        ], check=True)
        self.send_response(200)
        self.end_headers()

HTTPServer(("127.0.0.1", 9095), Handler).serve_forever()
```

Alertmanager posts to this handler through a `webhook_configs` receiver, so run it with `NYLAS_API_KEY`, `NYLAS_GRANT_ID`, and `ALERT_EMAIL_TO` in its environment, behind your routing tree. Install the CLI with `brew install nylas/nylas-cli/nylas` (see [getting started](https://cli.nylas.com/guides/getting-started) for other methods).

## How do I route alerts by severity?

Severity routing sends critical pages to one address and warnings to another, using Alertmanager's route tree rather than handler logic. You match on the `severity` label and assign a different receiver per branch. Each receiver points at the same handler on a distinct path or port, so the handler reads which recipient list to use from the URL it was hit on.

The route below splits traffic: alerts labeled `severity: critical` hit `/alert/critical`, everything else falls through to the default. This keeps the 2 audiences separate without duplicating alert rules. A critical database-down alert reaches the on-call rotation in under 60 seconds, while a disk-at-80-percent warning goes to a lower-priority inbox the team reviews daily.

```yaml
route:
  receiver: nylas-default
  routes:
    - matchers: [ 'severity="critical"' ]
      receiver: nylas-critical

receivers:
  - name: nylas-default
    webhook_configs:
      - url: 'http://localhost:9095/alert'
        send_resolved: true
  - name: nylas-critical
    webhook_configs:
      - url: 'http://localhost:9095/alert/critical'
        send_resolved: true
```

## Why does the handler need to track send failures?

A failed email send must not look like a healthy alert pipeline. If the handler returns HTTP 200 after `nylas email send` exits non-zero, Alertmanager marks the notification delivered and never retries. Returning a 5xx on send failure makes Alertmanager retry on its next attempt, governed by the queue and the route's `repeat_interval` of 4 hours in the earlier example.

The CLI prints a JSON object with the sent message ID when `--json` is passed, which gives you an audit record per alert email. Log that ID, then have the handler raise a 502 when the subprocess exits non-zero so the failure surfaces. Alertmanager's notification log dedupes resolved-and-firing pairs, so a brief outage on your side does not produce duplicate emails once delivery recovers. Pair email with a second channel for the highest-severity alerts so one delivery problem never hides a production incident.

```python
result = subprocess.run([
    "nylas", "email", "send", GRANT,
    "--to", TO, "--subject", subject, "--body", "\n".join(lines),
    "--yes", "--json",
], capture_output=True, text=True)

if result.returncode != 0:
    self.send_response(502)   # tell Alertmanager to retry
    self.end_headers()
    return

msg_id = json.loads(result.stdout).get("id", "unknown")
print(f"sent alert email: {msg_id}")
self.send_response(200)
self.end_headers()
```

## Next steps

- [Send Grafana Alert Emails without SMTP](https://cli.nylas.com/guides/grafana-alert-emails-cli) — Point a Grafana webhook contact point at a handler that runs nylas…
- [Send Sentry Issue Alerts by Email (CLI)](https://cli.nylas.com/guides/sentry-email-alerts-cli) — Turn a Sentry issue alert webhook into an enriched email with…
- [Terraform email alerts](https://cli.nylas.com/guides/terraform-email-alerts) – provision the alert sender and recipients as code
- [Kubernetes CronJob email](https://cli.nylas.com/guides/kubernetes-cronjob-email) – run the handler or scheduled checks inside a cluster
- [Debug email delivery from the CLI](https://cli.nylas.com/guides/debug-email-delivery-cli) – trace why an alert email never arrived
- [Automate email reports from terminal](https://cli.nylas.com/guides/automate-email-reports-terminal) – daily digests alongside real-time alerts
- [Email to Mattermost notifications](https://cli.nylas.com/guides/email-to-mattermost-notifications) – mirror the same alerts into chat
- [Email to Telegram notifications](https://cli.nylas.com/guides/email-to-telegram-notifications) – a second channel for critical pages
- [Command reference](https://cli.nylas.com/docs/commands) – `nylas email send` flags and JSON output
- [Alertmanager webhook_config reference](https://prometheus.io/docs/alerting/latest/configuration/#webhook_config) – receiver fields and payload format
- [Alertmanager overview](https://prometheus.io/docs/alerting/latest/alertmanager/) – grouping, routing, and silencing
- [RFC 5322](https://datatracker.ietf.org/doc/html/rfc5322) – the internet message format the CLI emits
