Grafana has no built-in SMS notifier — email, Slack and PagerDuty, but nothing that reaches a phone directly. This is a small webhook adapter that fixes that: Grafana posts an alert to it, and it sends an SMS or places a text-to-speech voice call through an SMSEagle gateway. One contact point handles both, and each rule decides which it gets.
The whole thing wired up end to end, from container to a phone ringing:
You configure one contact point. Which channel an alert uses, and who it reaches, are set per rule with labels — so on-call routing lives in the rule rather than in a pile of near-identical contact points.
| Label | Value | Result |
|---|---|---|
smseagle_channel | call, voice, tts |
Places a voice call that reads the alert aloud. |
smseagle_channel | sms, anything else, or absent |
Sends an SMS — the default. |
smseagle_to | +37120000000,+37120000001 |
Overrides recipients for this alert only. |
smseagle_to | absent | Falls back to the defaults in .env. |
A typo fails safe. voice and tts are aliases for
call; anything unrecognised — including cal — falls
through to SMS, so a slip sends a text rather than ringing someone at 3am.
In practice: put smseagle_channel: call on the handful of rules that
genuinely warrant waking someone, and leave it off everywhere else. A phone call is
much harder to sleep through than a text, which is exactly why it should be rare.
This is a device-bound path — Grafana's alerting engine must be able to reach the SMSEagle on your network. The clean fit is self-hosted Grafana on the same LAN as the gateway: no inbound exposure, no tunnel. Grafana Cloud cannot reach a private LAN address, so if you are on Cloud, use SMSEagle's Email2SMS poller instead.
First, make sure the SMSEagle API token carries the Send SMS permission — and Send calls too if you want voice. For calls, note a TTS voice model id under Calls → TTS Voice models.
git clone https://github.com/dmitrylambert/grafana-monitoring.git
cd grafana-monitoring/smseagle-alert-webhook
cp .env.example .env # device IP, API token, default recipients
docker compose up -d --build
curl http://localhost:9099/healthz # -> ok
Leave SMSEAGLE_TEST_MODE=true while you wire things up: SMSEagle validates
every request but delivers nothing, so you can test the whole path without sending real
messages. Flip it to false and rebuild when you are satisfied.
After editing .env or app.py, re-run
docker compose up -d --build. A plain restart keeps the old image, which
is the most common reason a change appears to do nothing.
In Alerting → Contact points → Add contact point, create a
Webhook integration named SMSEagle pointing at the adapter.
The URL depends on where Grafana runs:
| Grafana runs… | Contact point URL |
|---|---|
| In the same Docker network | http://smseagle-alert-webhook:9099/ |
| On the same host, port published | http://host.docker.internal:9099/ |
| Elsewhere on the LAN | http://<adapter-host-ip>:9099/ |
Then route alerts to it under Notification policies, and add
smseagle_channel: call to whichever rules should ring a phone. Provisioning
files for the contact point, policy and a sample rule are in
examples/grafana-provisioning/ if you would rather keep it in version
control.
The body is a status word followed by your alert's annotation, verbatim — no grouped-label preamble, no firing counts:
PROBLEM. SMS outbox > 20 on smseagle-gateway
RESOLVED. SMS outbox > 20 on smseagle-gateway
It takes the alert's message, summary or
description annotation, whichever is present first, so what you write on
the rule is exactly what gets read aloud. That matters far more for voice than for SMS:
hearing a machine recite label pairs and firing counts at 3am is genuinely unpleasant.
Grafana notifies on both firing and resolved, so by default a cleared alert produces a second message. Tick Disable resolved message on the contact point to stop that — it applies to both channels.
The contact point URL has to be reachable from inside the Grafana container.
There, http://localhost:9099 is Grafana itself, not the adapter. Use the
container name, or host.docker.internal with
extra_hosts: ["host.docker.internal:host-gateway"] on the Grafana service.
localhost only works when Grafana runs natively.
Grafana's test payload is synthetic and carries none of your rule's labels or
annotations, so smseagle_channel, smseagle_to and your message
text do not apply. Test with a real firing rule and watch
docker logs -f smseagle-alert-webhook.
Provisioned rules are read-only in the UI. Create the rule in the UI instead, or edit the provisioning YAML.
Getting alerts out is the easy half — routing them so the right person is woken and nobody else is, is the part that takes judgement. Tell me what your on-call looks like and I will reply with how I would wire it.