How to Receive Webhooks from Grafana Alerts (Self-Hosted, Step by Step)
Receive and verify Grafana alert webhooks on your own server: HMAC-SHA256 signature validation, Bearer fallback for older versions, Docker setup, and RabbitMQ routing, verified against a real Grafana 13.2.
By the end of this guide you will have a self-hosted endpoint that receives Grafana alert notifications, verifies the HMAC-SHA256 signature on every delivery, and routes the payloads to a log, RabbitMQ, or any other destination. Everything below was verified end to end against a real Grafana 13.2 instance and the real Core Webhook Module Docker image: Grafana's own alerting pipeline fired the test deliveries, so the headers, signatures, and responses you see are the ones you will get.
How Grafana webhook notifications work
Grafana Alerting sends notifications through contact points. A webhook contact point POSTs a JSON payload to your URL whenever a routed alert fires or resolves. The payload's top-level fields include receiver, status (firing or resolved), alerts (an array of alert objects with labels, annotations, startsAt, values, fingerprint), groupLabels, commonLabels, externalURL, title, and message. The request arrives with User-Agent: Grafana and Content-Type: application/json.
Authentication options on the webhook contact point:
- HMAC signature (recent Grafana versions, verified here on 13.2): Grafana signs the raw request body with HMAC-SHA256 and puts the plain lowercase hex digest, no prefix, in a header. The default header is
X-Grafana-Alerting-Signature. In Grafana's source the signature is exactlyhex(HMAC-SHA256(secret, body)). - Authorization header: a scheme (default
Bearer) plus credentials, sent as a standardAuthorizationheader. Use this on older Grafana versions without the HMAC option. - HTTP Basic Authentication: username and password. Grafana allows either Basic auth or the Authorization header, not both.
One critical detail for the HMAC option: the contact point also has a Timestamp Header field. If you fill it in, Grafana signs timestamp:body instead of the body alone. Leave it empty for this setup; the gateway verifies the signature over the raw body.
Prerequisites
- Docker on any machine (your laptop is fine for the test run)
- A public HTTPS URL for production, unless Grafana and your receiver share a network. Two ways to get one:
- HTTPS for webhooks with nginx and Let's Encrypt if you have a public server
- Receiving webhooks through Cloudflare Tunnel if you do not want to open inbound ports
Step 1: Configure the receiver
Create a working directory with two files.
webhooks.json:
{
"grafana_alerts": {
"data_type": "json",
"module": "log",
"hmac": {
"secret": "{$GRAFANA_WEBHOOK_SECRET}",
"header": "X-Grafana-Alerting-Signature",
"algorithm": "sha256"
}
}
}
connections.json:
{}
What each parameter does:
grafana_alertsis the webhook ID; it becomes the URL path (/webhook/grafana_alerts).module: logprints each verified payload to the container log. Zero dependencies for the first run; we swap in RabbitMQ at the end.hmacrecomputes the HMAC-SHA256 of the raw body and compares it against theX-Grafana-Alerting-Signatureheader in constant time. Grafana sends the digest as plain hex, which is accepted as-is; header lookup is case-insensitive.{$GRAFANA_WEBHOOK_SECRET}pulls the secret from an environment variable, so it never lives in a config file.
On an older Grafana without the HMAC signature option, use the Authorization header instead. This variant was verified too:
{
"grafana_alerts": {
"data_type": "json",
"module": "log",
"authorization": "Bearer {$GRAFANA_WEBHOOK_TOKEN}"
}
}
Set the contact point's Authentication Header Scheme to Bearer and paste the same token as the credentials. A wrong token gets 401 {"detail":"Unauthorized"}.
Generate a strong secret (or use our webhook secret generator):
openssl rand -hex 32
Step 2: Run it
docker run -d --name webhook-gateway -p 8000:8000 \
-v "$PWD/webhooks.json:/app/webhooks.json:ro" \
-v "$PWD/connections.json:/app/connections.json:ro" \
-e GRAFANA_WEBHOOK_SECRET="your-generated-secret" \
spiderhash/webhook:latest
Confirm it is up:
docker logs webhook-gateway
# ... INFO: Application startup complete.
The receiver now answers on http://localhost:8000/webhook/grafana_alerts, with auto-generated API docs at http://localhost:8000/docs.
Step 3: Test it locally
You do not need Grafana to test; you need a request signed the way Grafana signs. Save a realistic alert payload:
cat > alert.json <<'EOF'
{"receiver":"cwm-gateway","status":"firing","orgId":1,"alerts":[{"status":"firing","labels":{"alertname":"HighCPU","grafana_folder":"ops","instance":"web-01"},"annotations":{"summary":"CPU above 90% for 5 minutes"},"startsAt":"2026-08-29T10:15:00Z","endsAt":"0001-01-01T00:00:00Z","values":{"A":96.2},"fingerprint":"a3c5de8f1b24c9d7"}],"groupLabels":{"alertname":"HighCPU"},"commonLabels":{"alertname":"HighCPU","instance":"web-01"},"commonAnnotations":{"summary":"CPU above 90% for 5 minutes"},"externalURL":"https://grafana.example.com/","version":"1","groupKey":"{}:{alertname=\"HighCPU\"}","truncatedAlerts":0,"state":"alerting","title":"[FIRING:1] HighCPU web-01","message":"**Firing**\n\nValue: A=96.2"}
EOF
Compute the signature over the exact bytes of the file and send it as plain hex, exactly like Grafana does:
SIG=$(openssl dgst -sha256 -hmac "your-generated-secret" -hex alert.json | awk '{print $NF}')
curl -s -w "\nHTTP %{http_code}\n" -X POST http://localhost:8000/webhook/grafana_alerts \
-H "Content-Type: application/json" \
-H "X-Grafana-Alerting-Signature: $SIG" \
--data-binary @alert.json
Expected result:
{"message":"200 OK"}
HTTP 200
Now prove the verification works. A tampered or wrong signature:
{"detail":"Invalid HMAC signature"}
HTTP 401
And a request with no signature header at all:
{"detail":"Missing X-Grafana-Alerting-Signature header"}
HTTP 401
Those are the exact responses the gateway returns. docker logs webhook-gateway shows the accepted payload.
Step 4: Create the contact point in Grafana
In Grafana: Alerts & IRM > Alerting > Contact points > + Add contact point, choose the Webhook integration:
- URL:
https://your-domain/webhook/grafana_alerts - Under Optional Webhook settings, find the HMAC signature section:
- Secret: the same value you passed as
GRAFANA_WEBHOOK_SECRET - Header: leave the default
X-Grafana-Alerting-Signature - Timestamp Header: leave empty (see above)
- Leave HTTP Method as
POST
Save, then use the contact point's Test button to send a test notification, and route real alerts to it via your notification policy (Alerting > Notification policies). Every delivery from a real firing alert arrives signed; in this guide's verification run, an always-firing test rule delivered within seconds of the evaluation interval and the gateway accepted it with 200.
Troubleshooting
401 {"detail":"Invalid HMAC signature"}: the Secret in the contact point andGRAFANA_WEBHOOK_SECRETdiffer, or the Timestamp Header field is set. With a timestamp header configured, Grafana signstimestamp:bodyinstead of the body, and verification over the raw body fails by design. Clear the field.401 {"detail":"Missing X-Grafana-Alerting-Signature header"}: the HMAC section is not filled in, or a custom Header name does not match theheadervalue inwebhooks.json. They must be identical.- No HMAC section in your Grafana: the HMAC signature option is a recent addition (this guide verified it on Grafana 13.2). On older versions use the Authorization header variant shown in Step 1.
- One notification for several alerts: Grafana groups alerts per notification policy; the
alertsarray can contain many entries, andtruncatedAlertstells you if the Max Alerts contact point setting cut any off.
Going to production
Printing to logs is not a pipeline. Swap the module for a real destination; here is the RabbitMQ variant, verified end to end (a real firing Grafana alert landed as a message in the grafana_alerts queue):
webhooks.json:
{
"grafana_alerts": {
"data_type": "json",
"module": "rabbitmq",
"connection": "rabbitmq_local",
"module-config": {
"queue_name": "grafana_alerts"
},
"hmac": {
"secret": "{$GRAFANA_WEBHOOK_SECRET}",
"header": "X-Grafana-Alerting-Signature",
"algorithm": "sha256"
}
}
}
connections.json:
{
"rabbitmq_local": {
"type": "rabbitmq",
"host": "{$RABBITMQ_HOST:localhost}",
"port": "{$RABBITMQ_PORT:5672}",
"user": "{$RABBITMQ_USER:guest}",
"pass": "{$RABBITMQ_PASS:guest}"
}
}
The same pattern works for 17 other destinations: archive alert history to S3, publish to Kafka, or fan out to several at once with webhook chaining. Built-in rate limiting and retry handling cover the operational edges; an alert storm that groups hundreds of notifications is exactly when you want the gateway acknowledging fast and queueing durably. The full authentication reference, including HMAC validation, Bearer tokens, and 10 other methods, is in the docs.