Outsharked

Vikunja Push Notifications via Home Assistant Webhooks

May 29, 2026
homelab
8 Minutes
1594 Words
Vikunja and Home Assistant

I had been looking around for a self-hosted task manager, and settled on Vikunja. I really like the clean UX and simple configuration. But it has one significant shortcoming out of the box: mobile push notification support is a bit underwhelming.

The native (iOS/Android) app does support push notifications for reminders, but they have not worked for me. There are a few open issues around this, so maybe it will improve, but updates to the app seems slow. Even if they worked, the app lacks the rich, fine-grained event support that Vikunja itself offers via webhooks. There’s no configuration; presumably we can only get notifications for reminders.

Finally, it also depends on being able to connect to your vikunja instance. If you have a VPN connection to your home network running all the time, that might not be a big deal, but it feels fragile to me. I want something that’s going to be more robust.

I found a couple of old conversations about possible solutions — options for push notifications on android, and a configuration using ntfy and NodeRed. There are some interesting approaches, but I really didn’t want to stand up new infrastructure or use new cloud services. All the options depend on a cloud service like ntfy, and adding new infrastructure or local services to integrate it.

Home Assistant to the rescue!

Vikunja task created notification Vikunja comment notification

But wait! I’m running Home Assistant, which opens a whole bunch of possibilities. I use Nabu Casa’s cloud service, so it’s always available. Without much effort, I was able to set up an integration so that Home Assistant can deliver notifications from Vikunja. I’ll still get them even if I can’t access Vikunja, because everything goes through HA.

This solution will:

  • Allow Vikunja to send push notifications via the Home Assistant companion app to your phone
  • Provide fine grained control over notifications using Vikunja’s webhooks
  • Notifications include the type of event, name of project/task, and contents of comments
  • Tapping on the notification will open the relevant task in Vikunja

What will actually work outside your network depends on what kind of remote access you have configured. If HA already works for you anywhere, you’ll get the notifications. If you can access your Vikunja instance, tapping on them will open the associated task. If you only have HA access but not Vikunja (e.g. you have Home Assistant cloud, but not some other general purpose VPN to access Vikunja on your network) you’ll still get the notifications, you just won’t be able to open them in Vikunja when you’re not in your home network.

Overview

The setup is pretty simple overall:

  • Create an automation in HA that exposes a webhook
  • Add webhooks in Vikunja for project or user-scoped events (or both) posting to the HA webhook
  • When an event occurs, the HA automation is triggered and generates a push notification via the HA companion app

Step 1: The HA Automation

In automations.yaml, add a webhook trigger automation. This automation config creates the webhook itself as /api/webhook/<your_webhook_id> — no other config is needed in HA.

You’ll need to replace these with the IPs of your services

  • 172.16.2.100 - Home Assistant
  • 172.16.2.101 - Vikunja
1
- id: vikunja-reminder-notification
2
alias: Vikunja reminder notification
3
triggers:
4
- trigger: webhook
5
webhook_id: vikunja_reminder
6
allowed_methods:
7
- POST
8
local_only: false
9
conditions: []
10
actions:
11
# replace with your device's notify service
12
- action: notify.mobile_app_pixel_10
13
data:
14
title: >-
15
{%- set prefix = trigger.json.data.project.title + ': ' if trigger.json.data.project is defined and
26 collapsed lines
16
trigger.json.data.project else '' -%} {{ prefix }}{{ trigger.json.event_name
17
| replace('.', ' ')
18
| replace('_', ' ')
19
| title
20
| default('Task Event', boolean=True) }}
21
message: >-
22
{%- if trigger.json.data.comment is defined and trigger.json.data.comment -%} {{ trigger.json.data.doer.name
23
}}: {{ trigger.json.data.comment.comment | truncate(200) }} {%- else -%} {{ trigger.json.data.task.title
24
| default(trigger.json.data.tasks | map(attribute='title') | join(', ')
25
| default('Task event')) }}
26
{%- endif %}
27
data:
28
icon_url: https://cdn.jsdelivr.net/gh/go-vikunja/vikunja/frontend/public/images/icons/icon-maskable.png
29
notification_icon: >-
30
{%- if 'comment' in trigger.json.event_name %}mdi:comment {%- elif 'reminder' in trigger.json.event_name
31
%}mdi:bell {%- elif 'overdue' in trigger.json.event_name %}mdi:clock-alert {%- elif 'deleted' in
32
trigger.json.event_name %}mdi:delete {%- elif 'assignee' in trigger.json.event_name %}mdi:account {%- else
33
%}mdi:checkbox-marked-circle{%- endif %}
34
# replace with your Vikunja URL
35
clickAction: >-
36
{%- if trigger.json.event_name == 'task.deleted' and trigger.json.data.project is defined and
37
trigger.json.data.project -%} https://your-vikunja-url/projects/{{ trigger.json.data.project.id }} {%- elif
38
trigger.json.data.comment is defined and trigger.json.data.comment -%} https://your-vikunja-url/tasks/{{
39
trigger.json.data.task.id }}#comment-{{ trigger.json.data.comment.id }} {%- else -%}
40
https://your-vikunja-url/tasks/{{ trigger.json.data.task.id }} {%- endif %}
41
mode: single

A few notes on the template. Vikunja’s webhook body looks like this:

1
{
2
"event_name": "task.reminder.fired",
3
"time": "2026-05-29T17:24:00Z",
4
"data": {
5
"task": { "id": 42, "title": "Call the plumber", ... },
6
"project": { ... },
7
"user": { ... }
8
}
9
}

For comment events (task.comment.created), the body also includes data.comment with the comment text and ID, and data.doer with the commenter’s name. data.task still contains the associated task.

The title template prepends the project name when available (e.g. “Homelab: Comment Created”), then replaces dots and underscores with spaces so the event name reads naturally — task.reminder.fired becomes “Reminder Fired”, task.comment.created becomes “Comment Created”, task.overdue becomes “Overdue”, etc. Project-level webhooks always include data.project in the payload (the Vikunja webhook listener injects it automatically for events that don’t carry it natively), so the prefix is reliable for project webhooks. For user-level webhooks (reminder/overdue), data.project is included in those event structs directly.

The message template checks for a comment first and, if present, shows the commenter’s name followed by the comment text (e.g. “Jamie: Don’t forget the attachment”). The truncate(200) guard prevents very long comments from hitting FCM’s 4KB payload limit, which would cause the notification to fail silently. Otherwise it falls back to data.task.title, then to the joined titles from the data.tasks array (used by tasks.overdue batch events).

The notification_icon sets the small status bar icon using any Material Design Icon name. It varies by event type — a bell for reminders, a comment bubble for comments, etc.

The icon_url shows a thumbnail image on the right side of the notification. Point it at a publicly accessible PNG — your Vikunja instance’s logo works if you’re always on your home network or VPN when you receive notifications, otherwise use a public URL. Note: this does not replace the HA logo on the left; Android doesn’t allow apps to change their own notification icon.

The clickAction opens the task directly when the notification is tapped. For deleted tasks it links to the project instead (since the task URL would 404). For comment events it appends #comment-<id> to link directly to the comment — though in practice Android’s browser context doesn’t scroll to anchors, so you land on the task page regardless. Still worth including for platforms that do support it.

local_only: false is required because the webhook comes from the Docker container’s network, which gets NAT’d through 172.16.2.101. Even though that’s on the same LAN as HA, HA needs local_only: false to accept it — the internal Docker bridge subnet (172.30.x.x) apparently doesn’t satisfy the “local” check.

Step 2: Configure Vikunja Webhooks

In Vikunja, go to Settings → Webhooks to add a user-level webhook (fires for all your projects):

  • URL: http://172.16.2.100:8123/api/webhook/vikunja_reminder
  • Events: task.overdue, task.reminder.fired, tasks.overdue

You can also create a webhook at the project level, which exposes a much wider set of events:

EventFires when
task.created / task.updated / task.deletedTask lifecycle
task.comment.created / task.comment.edited / task.comment.deletedComments
task.assignee.created / task.assignee.deletedAssignment changes
task.attachment.created / task.attachment.deletedFile attachments
task.relation.created / task.relation.deletedTask relations
task.reminder.firedReminder time reached
task.overdue / tasks.overdueOverdue (single or batch)
project.created / project.updated / project.deletedProject lifecycle
project.shared.user / project.shared.teamSharing

The automation handles all of these with the same template — just add whichever events you want to subscribe to in the Vikunja webhook config.

Step 3: The SSRF Problem

Vikunja has SSRF protection that blocks outgoing webhook requests to private IP ranges — including 172.16.0.0/12, which covers my entire home network. So every time a reminder fired, the webhook attempt was logged as:

1
level=ERROR msg="Error while handling message ...
2
reason_poisoned=Post "http://172.16.2.100:8123/...":
3
dial tcp 172.16.2.100:8123: prohibited IP address:
4
172.16.2.100 is not a permitted destination (denied by: 172.16.0.0/12)"

There’s a config option to disable this check: outgoingrequests.allownonroutableips. Add it to your docker-compose.yml environment block:

1
services:
2
vikunja:
3
image: vikunja/vikunja:latest
4
environment:
5
# ... other config ...
6
VIKUNJA_OUTGOINGREQUESTS_ALLOWNONROUTABLEIPS: 'true'

With this set, Vikunja dispatches the webhook, HA receives it, and the push notification arrives.

The Template Gotcha

One more thing worth documenting. The Watermill internal event message (visible in Vikunja’s error logs) looks like {"webhook_id":3,"payload":{"event_name":"...","data":{...}}}. This is not what Vikunja actually sends over HTTP. The real HTTP body is just the WebhookPayload struct:

1
{
2
"event_name": "task.created",
3
"time": "...",
4
"data": { "task": { ... } }
5
}

So the correct HA template path is trigger.json.data.task.title, not trigger.json.payload.data.task.title. Getting this wrong produces a huge JSON blob as the fallback message, which either fails silently or produces an unusable notification.

Final State

The automation triggers reliably for Vikunja events. The notification title shows the event type formatted as readable text, the message shows the task title or comment text (with attribution) depending on the event, and tapping the notification opens the task directly in Vikunja. Setting reminder times on tasks works as expected — Vikunja’s reminder cron checks every minute and fires the webhook when one is due.

The only remaining limitation is that reminders require the reminder time to fall in the current minute in the user’s configured timezone. If you set a reminder and nothing fires, double-check that Vikunja’s timezone config matches your local time — the reminder cron runs in UTC by default and the timezone adjustment happens per-user at dispatch time.

More Home Assistant Integration

This configuration is only about getting push notifications for Vikunja events. I use a VPN to access Vikunja in my homelab directly on the road. If you want more direct integration, check out the Vikunja Home Assistant Integration.

Article title:Vikunja Push Notifications via Home Assistant Webhooks
Article author:Jamie
Release time:May 29, 2026
Copyright 2026
Sitemap