How to Receive Webhooks from Metabase Alerts (Self-Hosted, Step by Step)

Receive Metabase alert webhooks on your own server: Bearer authentication, the host-strategy SSRF setting, the empty-body test ping gotcha, Docker setup, and RabbitMQ routing, verified against a real Metabase alert.

By the end of this guide you will have a self-hosted endpoint that receives Metabase alert webhooks, authenticates every delivery with a Bearer token, and routes the payloads to a log, RabbitMQ, or any other destination. Everything below was verified end to end against a real Metabase instance and the real Core Webhook Module Docker image: an actual Metabase alert sent the deliveries quoted here, and this run surfaced two behaviors worth knowing before you start.

How Metabase webhooks work

Metabase sends webhooks through notification channels: an admin creates a webhook under Admin > Settings > Webhooks ("Webhooks for alerts"), and users can then pick it as the destination of an alert on a question. Webhooks are only available for alerts; dashboard subscriptions cannot target them.

A real alert delivery looks like this (captured live; the base64 PNG shortened for print):

{
  "type": "alert",
  "alert_id": 1,
  "alert_creator_id": 1,
  "alert_creator_name": "Admin User",
  "data": {
    "type": "question",
    "question_id": 40,
    "question_name": "Order count",
    "question_url": "http://metabase.example.com/question/40",
    "visualization": "data:image/png;base64,iVBORw0KG...",
    "raw_data": {"cols": ["count"], "rows": [[18760]]}
  },
  "sent_at": "2026-08-29T20:06:58.529002959Z"
}

visualization is a base64-encoded PNG of the chart and raw_data carries the actual result rows, so the webhook consumer gets both the picture and the numbers. The request arrives with Content-Type: application/json and an Apache HttpClient user agent.

Authentication: the webhook form's Authentication method offers None, Basic (username/password), Bearer (secret token), and API key (in a header or query param). With Bearer, Metabase sends exactly Authorization: Bearer <your token> (confirmed by capturing a live request), which the gateway compares in constant time.

Two behaviors verified against a live instance:

  1. Private-network targets are blocked by default. Metabase's http-channel-host-strategy setting defaults to external-only; a webhook URL that resolves to a private or local address is rejected with "URLs referring to hosts that supply internal hosting metadata are prohibited." For a receiver on your own network, start Metabase with MB_HTTP_CHANNEL_HOST_STRATEGY=allow-private (or allow-all to include localhost).
  2. The connection test sends an empty body. The "Send a test" ping is a POST with Content-Length: 0 and only the auth header. A receiver that validates JSON will reject it with 400 even though authentication succeeded, so a failed test against this gateway does not mean the webhook is broken; a real alert delivers a full JSON payload and succeeds.

Prerequisites

Step 1: Configure the receiver

Create a working directory with two files.

webhooks.json:

{
    "metabase_alerts": {
        "data_type": "json",
        "module": "log",
        "authorization": "Bearer {$METABASE_WEBHOOK_TOKEN}"
    }
}

connections.json:

{}

What each parameter does:

  • metabase_alerts is the webhook ID; it becomes the URL path (/webhook/metabase_alerts).
  • module: log prints each authenticated payload to the container log. Zero dependencies for the first run; we swap in RabbitMQ at the end.
  • authorization requires the exact Authorization: Bearer <token> header Metabase sends and compares it in constant time.
  • {$METABASE_WEBHOOK_TOKEN} pulls the token from an environment variable, so it never lives in a config file.

Generate a strong token (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 METABASE_WEBHOOK_TOKEN="your-generated-token" \
  spiderhash/webhook:latest

Confirm it is up:

docker logs webhook-gateway
# ... INFO: Application startup complete.

The receiver now answers on http://localhost:8000/webhook/metabase_alerts, with auto-generated API docs at http://localhost:8000/docs.

Step 3: Test it locally

Simulate a delivery. Save the alert payload from the top of this article as alert_payload.json, then:

curl -s -w "\nHTTP %{http_code}\n" -X POST http://localhost:8000/webhook/metabase_alerts \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer your-generated-token" \
  --data-binary @alert_payload.json

Expected result:

{"message":"200 OK"}
HTTP 200

A wrong token:

{"detail":"Unauthorized"}
HTTP 401

And a request with no Authorization header at all:

{"detail":"Invalid Bearer token format: must start with 'Bearer '"}
HTTP 401

Those are the exact responses the gateway returns. docker logs webhook-gateway shows the accepted payload.

Step 4: Create the webhook and alert in Metabase

In Admin > Settings > Webhooks, add a webhook:

  • Webhook URL: https://your-domain/webhook/metabase_alerts
  • Give it a name: something like webhook-gateway, plus a description
  • Authentication method: Bearer, with the same token you passed as METABASE_WEBHOOK_TOKEN

Then open any saved question, create an Alert on it, and choose your webhook as the destination. When the alert's condition fires (or on its schedule), the delivery lands; docker logs webhook-gateway shows the full payload with the chart and rows. In this guide's verification run, a real alert send was accepted with 200 within a second.

Troubleshooting

  • "URLs referring to hosts that supply internal hosting metadata are prohibited" when saving or testing: your URL resolves to a private or local address and http-channel-host-strategy is at its external-only default. Set MB_HTTP_CHANNEL_HOST_STRATEGY=allow-private on the Metabase container (verified: the identical webhook worked the moment the setting changed) or use a public HTTPS URL.
  • "Failed to connect to channel" on Send a test, while real alerts work: the test ping has an empty body, and this gateway validates JSON, answering 400 {"detail":"Malformed JSON payload"}. Authentication already succeeded at that point; trigger a real alert to see the full flow.
  • 401 {"detail":"Unauthorized"}: the Bearer token in the webhook's Authentication section and METABASE_WEBHOOK_TOKEN differ. They must be byte-identical.
  • Large payloads: visualization embeds a PNG, so deliveries run kilobytes to megabytes depending on the chart. Budget accordingly in downstream queues.

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 Metabase alert landed as a message in the metabase_alerts queue):

webhooks.json:

{
    "metabase_alerts": {
        "data_type": "json",
        "module": "rabbitmq",
        "connection": "rabbitmq_local",
        "module-config": {
            "queue_name": "metabase_alerts"
        },
        "authorization": "Bearer {$METABASE_WEBHOOK_TOKEN}"
    }
}

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. The full authentication reference, including Bearer tokens, Basic auth, and 10 other methods, is in the docs.


Keep reading

Browse more: all integration guides · webhook security · HTTPS setup · documentation · what is Core Webhook Module?

Subscribe to Free Webhook Tool

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