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

Receive webhooks sent by Home Assistant automations via rest_command: secret header authentication, templated JSON payloads, Docker setup, and RabbitMQ routing, verified against a real Home Assistant instance.

By the end of this guide you will have a self-hosted endpoint that receives webhooks from your Home Assistant automations, authenticates every delivery with a secret header, and routes the events to a log, RabbitMQ, or any other destination. Everything below was verified end to end against a real Home Assistant instance (2026.8) and the real Core Webhook Module Docker image: an actual rest_command call from Home Assistant sent the deliveries quoted here.

How Home Assistant sends webhooks

Home Assistant is famous for receiving webhooks, but the traffic flows the other way too: an automation that tells your own systems something happened - a door opened, a backup finished, a sensor crossed a threshold. The built-in tool for that is the rest_command integration: you define named HTTP calls in configuration.yaml, then trigger them from automations, scripts, or Developer Tools as actions named rest_command.<name>.

What a delivery looks like on the wire (captured from a real call): a POST with User-Agent: HomeAssistant/2026.8.3 aiohttp/..., your configured Content-Type, your custom headers, and a body rendered from your payload template. There is no payload signing; authentication is whatever you configure, which is why the receiver should require a secret header and compare it in constant time.

The rest_command fields that matter here, from the official docs: url (templatable, required), method (defaults to get, so set post explicitly), headers (a map, where values can come from secrets.yaml via !secret), payload (a template), content_type, and timeout (default 10 seconds). Calls return status, content, and headers, readable in automations through response_variable.

Prerequisites

Step 1: Configure the receiver

Create a working directory with two files.

webhooks.json:

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

connections.json:

{}

What each parameter does:

  • ha_events is the webhook ID; it becomes the URL path (/webhook/ha_events).
  • module: log prints each authenticated event 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. case_sensitive: true makes the token value match exactly.
  • {$HA_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 HA_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/ha_events, with auto-generated API docs at http://localhost:8000/docs.

Step 3: Test it locally

Simulate exactly what Home Assistant will send:

cat > event.json <<'EOF'
{"event": "automation_triggered", "entity": "binary_sensor.front_door", "state": "on", "source": "home-assistant"}
EOF

curl -s -w "\nHTTP %{http_code}\n" -X POST http://localhost:8000/webhook/ha_events \
  -H "Content-Type: application/json" \
  -H "X-Webhook-Token: your-generated-token" \
  --data-binary @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: Configure Home Assistant

Put the token in secrets.yaml so it stays out of your main config:

gateway_webhook_token: "your-generated-token"

Then define the command in configuration.yaml. This exact block is what the verification run used:

rest_command:
  notify_gateway:
    url: "https://your-domain/webhook/ha_events"
    method: post
    content_type: "application/json"
    headers:
      X-Webhook-Token: !secret gateway_webhook_token
    payload: '{"event": "{{ event }}", "entity": "{{ entity }}", "state": "{{ state }}", "source": "home-assistant"}'

Restart Home Assistant (or reload REST commands from Developer Tools > YAML). The {{ event }}, {{ entity }}, and {{ state }} templates are filled by whatever data you pass when calling the action.

Use it from an automation:

automation:
  - alias: "Notify gateway when the front door opens"
    triggers:
      - trigger: state
        entity_id: binary_sensor.front_door
        to: "on"
    actions:
      - action: rest_command.notify_gateway
        data:
          event: automation_triggered
          entity: binary_sensor.front_door
          state: "on"

To test without waiting for a trigger, open Developer Tools > Actions, pick rest_command.notify_gateway, and pass the same data fields. In this guide's verification run that call delivered within a second and the gateway answered 200 OK; docker logs webhook-gateway shows the rendered payload arriving with the Home Assistant user agent.

Troubleshooting

  • 401 {"detail":"Invalid API key in header: X-Webhook-Token"}: the value behind !secret gateway_webhook_token and HA_WEBHOOK_TOKEN differ. They must be byte-identical.
  • 401 {"detail":"Missing required header: X-Webhook-Token"}: the headers block is missing or mis-indented under the command, or a proxy strips custom headers.
  • The request arrives as GET with no body: method defaults to get in rest_command. Set method: post explicitly.
  • The body is not valid JSON: the payload template must render to JSON, quotes included; a template value containing a double quote breaks it. Keep payload values simple, or build the automation's data values accordingly. Setting content_type: "application/json" matters too; without it the gateway's JSON parsing has nothing to go on.
  • Slow or unreachable receiver: rest_command gives up after timeout (10 seconds by default) and just logs an error; there are no retries. If the event must not be lost, the receiver should be highly available and hand off durably (next section).

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 Home Assistant rest_command call landed as a message in the ha_events queue):

webhooks.json:

{
    "ha_events": {
        "data_type": "json",
        "module": "rabbitmq",
        "connection": "rabbitmq_local",
        "module-config": {
            "queue_name": "ha_events"
        },
        "header_auth": {
            "header_name": "X-Webhook-Token",
            "api_key": "{$HA_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 home events 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; since rest_command itself never retries, the durable handoff on the receiving side is what makes the notification reliable. 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