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 exactly hex(HMAC-SHA256(secret, body)).
  • Authorization header: a scheme (default Bearer) plus credentials, sent as a standard Authorization header. 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

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_alerts is the webhook ID; it becomes the URL path (/webhook/grafana_alerts).
  • module: log prints each verified payload to the container log. Zero dependencies for the first run; we swap in RabbitMQ at the end.
  • hmac recomputes the HMAC-SHA256 of the raw body and compares it against the X-Grafana-Alerting-Signature header 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 and GRAFANA_WEBHOOK_SECRET differ, or the Timestamp Header field is set. With a timestamp header configured, Grafana signs timestamp:body instead 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 the header value in webhooks.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 alerts array can contain many entries, and truncatedAlerts tells 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.

Subscribe to Free Webhook Tool

Sign up now to get access to the library of members-only issues.
Jamie Larson
Subscribe