Home Automation

Configure Home Assistant Notifications via Webhook Server

Tech Setup6 min read
TS

Tech Setup

Published October 16, 2026 · Editorial policy

Configure Home Assistant Notifications via Webhook Server

You have a self-hosted backend service—perhaps a CI/CD runner, an error-tracking daemon, or a custom deployment script—and you need it to ping your phone when something breaks. Native push notifications from external servers usually require cloud relays, third-party subscriptions, or brittle API keys, but since you already run Home Assistant locally, you can bypass all of that by turning your instance into a secure webhook receiver. By the end of this guide, your self-hosted server will send a JSON payload over HTTP POST to Home Assistant, triggering a localized, high-priority notification on your mobile device without exposing your entire smart home to the public internet.

The Architecture at a Glance

Before writing code, it helps to map out the data flow. Your setup relies on three moving parts:

  1. The Self-Hosted Server: The origin of the event (e.g., a Node.js script, a Python daemon, or a Bash script using curl).
  2. Home Assistant Webhook Integration: An automation trigger that listens for a specific, cryptographically obscure webhook ID.
  3. The Companion App / Notification Service: The target device that renders the incoming payload as a rich push notification.
+------------------------+      HTTP POST (JSON)      +------------------------+      Persistent Connection      +------------------------+
| Self-Hosted Webhook    | -------------------------> | Home Assistant Webhook | ------------------------------> | Companion App (Phone)  |
| Source (e.g., CI/CD)   |                            | Automation Trigger     |                                 | (APNs / FCM)           |
+------------------------+                            +------------------------+                                 +------------------------+

Because Home Assistant handles the translation from an incoming webhook to an actionable notification service call, your backend server does not need to know anything about Apple Push Notification service (APNs) or Firebase Cloud Messaging (FCM). It just talks HTTP.

Step 1: Generating and Securing the Webhook ID

Home Assistant webhooks are handled natively by automations. You do not need to install custom components or edit configuration.yaml.

Navigate to your Home Assistant dashboard, go to Settings > Automations & Scenes, and click Create Automation. Choose Create new automation.

For the trigger, configure the following parameters:

  • Trigger type: Webhook
  • Webhook ID: Click generate (or create a high-entropy random string, e.g., prod_deploy_alert_99a8b2f7c4).
  • Local only: Uncheck this box if your self-hosted server resides on a different VLAN or a separate cloud VPS. Keep it checked only if the server lives on the exact same Docker bridge network or subnet and can route directly via Home Assistant's local IP.

Your automation YAML for the trigger block will look like this:

alias: "Webhook: Production Deployment Alert"
description: "Receives deployment status updates from self-hosted CI server"
trigger:
  - platform: webhook
    webhook_id: prod_deploy_alert_99a8b2f7c4
    local_only: false
condition: []

Save this automation temporarily without adding actions. You now have an active endpoint listening at:

http://<YOUR_HA_IP>:8123/api/webhook/prod_deploy_alert_99a8b2f7c4

Step 2: Structuring the Automation Actions

When the webhook fires, Home Assistant captures the incoming JSON payload and makes it available inside your automation via the trigger.json variable. You will extract data fields like title, message, and level from your server and pass them directly to your notification service.

Open your automation in the YAML editor and append the action block:

action:
  - service: notify.mobile_app_pixel_7_pro
    data:
      title: "{{ trigger.json.title | default('System Alert') }}"
      message: "{{ trigger.json.message | default('No message provided.') }}"
      data:
        ttl: 0
        priority: high
        channel: "{{ trigger.json.channel | default('general') }}"
        tag: "{{ trigger.json.tag | default('server_notification') }}"
        click_action: "{{ trigger.json.url | default('https://homeassistant.local') }}"

Understanding the Template Variables

  • trigger.json.title: Dynamically pulls the title sent by your webhook payload. The Jinja2 filter | default(...) prevents automation crashes if your server sends an incomplete payload.
  • ttl: 0 and priority: high: Forces the mobile operating system to wake the device immediately, bypassing doze modes or battery optimization delays. Essential for critical alerts.
  • channel: Groups notifications on Android devices, allowing you to assign custom vibration patterns or sound overrides per channel inside your Home Assistant companion app settings.
  • click_action: Deep-links the user directly to a specific dashboard view or external URL when they tap the notification banner.

Save and enable the automation.

Step 3: Dispatching Payloads from Your Server

With the endpoint listening, you can now trigger notifications from any environment capable of making an HTTP POST request. Below are implementations for the three most common self-hosted execution environments.

The Bash and cURL Approach

If you are running simple deployment scripts or cron jobs, a single curl command is all you need. Ensure your JSON payload is properly escaped.

curl -X POST \
  -H "Content-Type: application/json" \
  -d '{
        "title": "Deployment Successful",
        "message": "v2.4.1 deployed to production cluster with 0 errors.",
        "channel": "deployments",
        "tag": "deploy-v241",
        "url": "https://grafana.internal.net"
      }' \
  http://homeassistant.local:8123/api/webhook/prod_deploy_alert_99a8b2f7c4

Expected response from Home Assistant upon a successful trigger:

{}

An empty JSON object 200 OK indicates that the webhook successfully cleared authentication and fired the underlying automation engine.

The Node.js / TypeScript Implementation

For modern self-hosted Node applications, use the native fetch API (available in Node.js 18+). Create a utility function to cleanly abstract your alerting logic:

// notifier.ts
interface WebhookAlertPayload {
  title: string;
  message: string;
  channel?: string;
  tag?: string;
  url?: string;
}

const HA_WEBHOOK_URL = process.env.HA_WEBHOOK_URL || 'http://homeassistant.local:8123/api/webhook/prod_deploy_alert_99a8b2f7c4';

export async function sendHomeAssistantAlert(payload: WebhookAlertPayload): Promise<void> {
  try {
    const response = await fetch(HA_WEBHOOK_URL, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify(payload),
    });

    if (!response.ok) {
      throw new Error(`Home Assistant webhook returned status ${response.status}`);
    }
    
    console.log('Home Assistant notification dispatched successfully.');
  } catch (error) {
    console.error('Failed to dispatch Home Assistant webhook:', error);
  }
}

// Example usage:
// sendHomeAssistantAlert({
//   title: 'Database Backup Complete',
//   message: 'postgres_dump_2023_10_24.sql.gz synced to S3 (4.2GB)',
//   channel: 'backups'
// });

The Python Implementation

If your server runs Python daemons or a Django/Flask backend, use the requests library to push alerts.

import os
import requests

HA_WEBHOOK_URL = os.getenv(
    "HA_WEBHOOK_URL",
    "http://homeassistant.local:8123/api/webhook/prod_deploy_alert_99a8b2f7c4",
)


def send_ha_notification(title: str, message: str, channel: str = "general") -> None:
    payload = {"title": title, "message": message, "channel": channel}

    try:
        response = requests.post(HA_WEBHOOK_URL, json=payload, timeout=5)
        response.raise_for_status()
        print("Webhook delivered successfully.")
    except requests.exceptions.RequestException as e:
        print(f"Error dispatching webhook to Home Assistant: {e}")


if __name__ == "__main__":
    send_ha_notification(
        title="SSL Certificate Expiring",
        message="Wildcard cert for *.example.local expires in 4 days.",
        channel="security",
    )

Securing Your Webhook Endpoint in Production

Exposing a webhook endpoint directly over HTTP inside a local network is fine, but if your self-hosted server lives in a cloud data center (like AWS, Hetzner, or DigitalOcean) and needs to talk back to a home server, security requires extra hardening.

1. Enforce HTTPS via Reverse Proxy

Never expose Home Assistant's port 8123 directly to the public internet without SSL/TLS termination. Route incoming traffic through a reverse proxy like Nginx Proxy Manager, Caddy, or Cloudflare Tunnels.

Your external webhook URL will look like this:

https://hass.yourdomain.com/api/webhook/prod_deploy_alert_99a8b2f7c4

2. Implement Shared Secret Headers (Optional Hardening)

Because Home Assistant webhooks validate purely based on the URL path containing the webhook ID, anyone who sniffs or guesses your ID can trigger notifications. If you want absolute security, add an authorization header check to your automation:

Update your automation to check for a secret token:

trigger:
  - platform: webhook
    webhook_id: prod_deploy_alert_99a8b2f7c4
condition:
  - condition: template
    value_template: "{{ trigger.headers.get('authorization') == 'Bearer SuperSecretToken123' }}"

Then, update your server-side curl command to include the authorization header:

curl -X POST \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer SuperSecretToken123" \
  -d '{"title": "Secure Deploy", "message": "Build passed."}' \
  https://hass.yourdomain.com/api/webhook/prod_deploy_alert_99a8b2f7c4

Troubleshooting Common Issues

Even with clean configurations, network layers can introduce subtle bugs. Check these failure points if your notifications fail to arrive.

1. Getting a 404 Not Found Response

  • Cause: The webhook ID in your script does not match the webhook ID configured in the Home Assistant automation, or the automation is turned off.
  • Fix: Double-check the alphanumeric string in your URL. Go to Settings > Automations & Scenes in Home Assistant and ensure the toggle next to your webhook automation is explicitly enabled.

2. Getting a 405 Method Not Allowed Response

  • Cause: Your script is sending a GET request instead of a POST request.
  • Fix: Verify your client implementation explicitly sets the HTTP method to POST. Home Assistant webhooks reject all other verbs.

3. Automation Triggers, But No Phone Notification Appears

  • Cause: The notify.mobile_app_<device_name> service call targets a device identifier that no longer matches your active session. This frequently happens after reinstalling the Home Assistant Companion App or factory-resetting your phone.
  • Fix: Go to Settings > Devices & Services > Entities and search for notify.mobile_app_. Copy the exact, current entity ID and update it in your automation YAML action block.

Alternative Approaches and Prevention

If your self-hosted environment scales up and you find yourself managing dozens of disparate webhooks, maintaining individual automations inside Home Assistant can become tedious.

As an alternative to raw webhooks, consider deploying Gotify or Apprise as a dedicated notification microservice. Apprise, in particular, acts as a lightweight notification gateway that natively supports Home Assistant, Slack, Discord, and mobile push providers through a unified REST API.

However, if your goal is zero external dependencies, complete local privacy, and utilizing your existing Home Assistant hardware without spinning up intermediate messaging queues, the native webhook method covered here provides a resilient, high-performance bridge between your infrastructure and your pocket.