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
- Docker on any machine (your laptop is fine for the test run)
- A public HTTPS URL for production, unless Home Assistant 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:
{
"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_eventsis the webhook ID; it becomes the URL path (/webhook/ha_events).module: logprints each authenticated event to the container log. Zero dependencies for the first run; we swap in RabbitMQ at the end.header_authrequires theX-Webhook-Tokenheader on every request and compares it in constant time.case_sensitive: truemakes 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_tokenandHA_WEBHOOK_TOKENdiffer. They must be byte-identical.401 {"detail":"Missing required header: X-Webhook-Token"}: theheadersblock is missing or mis-indented under the command, or a proxy strips custom headers.- The request arrives as GET with no body:
methoddefaults togetinrest_command. Setmethod: postexplicitly. - The body is not valid JSON: the
payloadtemplate must render to JSON, quotes included; a template value containing a double quote breaks it. Keep payload values simple, or build the automation'sdatavalues accordingly. Settingcontent_type: "application/json"matters too; without it the gateway's JSON parsing has nothing to go on. - Slow or unreachable receiver:
rest_commandgives up aftertimeout(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.