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

Receive Healthchecks up/down alerts as webhooks on your own server: variable-templated bodies, secret header authentication, the private-IP setting, Docker setup, and RabbitMQ routing, verified against real check flips.

By the end of this guide you will have a self-hosted endpoint that receives Healthchecks webhook alerts, authenticates every delivery with a secret header, and routes the up/down events to a log, RabbitMQ, or any other destination. Everything below was verified end to end against a real self-hosted Healthchecks instance and the real Core Webhook Module Docker image: a real check went down and came back up, and both alerts are the deliveries described here.

How Healthchecks webhooks work

Healthchecks monitors cron jobs and scheduled tasks: your job pings a unique URL, and when the pings stop (or a /fail ping arrives), the check flips to down and Healthchecks notifies its integrations. The Webhook integration gives you full control over the outgoing request, separately for down and up events:

  • URL, HTTP method, and request body for down alerts
  • URL, method, and body for up (recovery) alerts
  • Request headers, applied to both

Bodies and URLs support variable substitution: $NAME (check name), $STATUS (down/up), $CODE (check UUID), $NOW, $TAGS, and more. A real down alert from this guide's verification run, with the body template {"event": "check_down", "check": "$NAME", "status": "$STATUS"}:

{"event": "check_down", "check": "nightly-backup", "status": "down"}

The request arrives with User-Agent: healthchecks.io and whatever Content-Type you set in the headers. There is no payload signing; authentication is the headers you configure, and the receiver compares the secret in constant time.

One behavior verified live: on a self-hosted instance, webhook targets on private IP addresses are refused with "Connections to private IP addresses are not allowed" unless the instance runs with INTEGRATIONS_ALLOW_PRIVATE_IPS=True.

Prerequisites

Step 1: Configure the receiver

Create a working directory with two files.

webhooks.json:

{
    "healthchecks_events": {
        "data_type": "json",
        "module": "log",
        "header_auth": {
            "header_name": "X-Webhook-Token",
            "api_key": "{$HC_WEBHOOK_TOKEN}",
            "case_sensitive": true
        }
    }
}

connections.json:

{}

What each parameter does:

  • healthchecks_events is the webhook ID; it becomes the URL path (/webhook/healthchecks_events).
  • module: log prints each authenticated alert to the container log. Zero dependencies for the first run; we swap in RabbitMQ at the end.
  • header_auth requires the X-Webhook-Token header on every request and compares it in constant time.
  • {$HC_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 HC_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/healthchecks_events, with auto-generated API docs at http://localhost:8000/docs.

Step 3: Test it locally

Simulate exactly what a down alert sends:

cat > down_event.json <<'EOF'
{"event": "check_down", "check": "nightly-backup", "status": "down"}
EOF

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

Expected result:

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

Now prove the authentication works. A wrong token:

{"detail":"Invalid API key in header: X-Webhook-Token"}
HTTP 401

And a request with no token header at all:

{"detail":"Missing required header: X-Webhook-Token"}
HTTP 401

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

Step 4: Create the integration in Healthchecks

In Healthchecks: Integrations > Webhook > Add Integration:

  • URL for "down" events: https://your-domain/webhook/healthchecks_events, method POST, request body:
{"event": "check_down", "check": "$NAME", "status": "$STATUS"}
  • URL for "up" events: same URL, method POST, request body:
{"event": "check_up", "check": "$NAME", "status": "$STATUS"}
  • Request headers (applied to both):
X-Webhook-Token: your-generated-token
Content-Type: application/json

Save, then assign the integration to your checks (each check's page lists integrations with on/off toggles).

Test the whole loop like the verification run did: ping the check once so it is up, then hit its /fail endpoint:

curl https://your-healthchecks-domain/ping/<check-uuid>
curl https://your-healthchecks-domain/ping/<check-uuid>/fail

The down alert lands within seconds; a subsequent successful ping delivers the up alert. Both appear in docker logs webhook-gateway with user-agent: healthchecks.io.

Troubleshooting

  • 401 {"detail":"Invalid API key in header: X-Webhook-Token"}: the header value in the integration and HC_WEBHOOK_TOKEN differ. They must be byte-identical.
  • "Connections to private IP addresses are not allowed" in the integration's log: the self-hosted instance refuses private targets by default. Set INTEGRATIONS_ALLOW_PRIVATE_IPS=True on the Healthchecks container (verified: the identical integration delivered the moment the instance restarted with it), or use a public HTTPS URL.
  • Down fires but up never does: the up URL and body are configured separately; leaving them empty means no recovery notification. Fill both sides.
  • The body arrives with literal $NAME: variable substitution only covers the documented $VARIABLES; check spelling and case.

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

webhooks.json:

{
    "healthchecks_events": {
        "data_type": "json",
        "module": "rabbitmq",
        "connection": "rabbitmq_local",
        "module-config": {
            "queue_name": "healthchecks_events"
        },
        "header_auth": {
            "header_name": "X-Webhook-Token",
            "api_key": "{$HC_WEBHOOK_TOKEN}",
            "case_sensitive": true
        }
    }
}

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 incident 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; when a shared outage flips dozens of checks at once, acknowledging fast and queueing durably keeps the alert trail complete. The full authentication reference, including header auth and 11 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