diff --git a/web/public/static/langs/bg.json b/web/public/static/langs/bg.json
index 8df20531..a4ecbf69 100644
--- a/web/public/static/langs/bg.json
+++ b/web/public/static/langs/bg.json
@@ -56,7 +56,7 @@
     "notifications_attachment_copy_url_button": "Копиране на адреса",
     "notifications_attachment_open_button": "Отваряне на прикачения файл",
     "notifications_attachment_link_expires": "препратката изтича на {{date}}",
-    "notifications_click_open_title": "Към {{url}}",
+    "notifications_actions_open_url_title": "Към {{url}}",
     "notifications_click_copy_url_button": "Копиране на препратка",
     "notifications_click_open_button": "Отваряне",
     "notifications_click_copy_url_title": "Копира препратката в междинната памет",
diff --git a/web/public/static/langs/de.json b/web/public/static/langs/de.json
index 3beca7f4..2be4707f 100644
--- a/web/public/static/langs/de.json
+++ b/web/public/static/langs/de.json
@@ -48,7 +48,7 @@
     "notifications_attachment_open_button": "Anhang öffnen",
     "notifications_attachment_link_expired": "Download-Link ist abgelaufen",
     "notifications_click_copy_url_button": "Link kopieren",
-    "notifications_click_open_title": "Gehe zu {{url}}",
+    "notifications_actions_open_url_title": "Gehe zu {{url}}",
     "publish_dialog_other_features": "Andere Optionen:",
     "notifications_none_for_topic_description": "Um Benachrichtigungen an dieses Thema zu senden, PUTe/POSTe an die Themen-URL.",
     "notifications_no_subscriptions_title": "Anscheinend hast Du noch keine Themen abonniert.",
diff --git a/web/public/static/langs/en.json b/web/public/static/langs/en.json
index a27ff867..d668ce2c 100644
--- a/web/public/static/langs/en.json
+++ b/web/public/static/langs/en.json
@@ -26,8 +26,10 @@
   "notifications_attachment_link_expired": "download link expired",
   "notifications_click_copy_url_title": "Copy link URL to clipboard",
   "notifications_click_copy_url_button": "Copy link",
-  "notifications_click_open_title": "Go to {{url}}",
   "notifications_click_open_button": "Open link",
+  "notifications_actions_open_url_title": "Go to {{url}}",
+  "notifications_actions_not_supported": "Action not supported in web app",
+  "notifications_actions_http_request_title": "Send HTTP {{method}} to {{url}}",
   "notifications_none_for_topic_title": "You haven't received any notifications for this topic yet.",
   "notifications_none_for_topic_description": "To send notifications to this topic, simply PUT or POST to the topic URL.",
   "notifications_none_for_any_title": "You haven't received any notifications.",
diff --git a/web/public/static/langs/es.json b/web/public/static/langs/es.json
index c4baa83f..1d8c62ca 100644
--- a/web/public/static/langs/es.json
+++ b/web/public/static/langs/es.json
@@ -26,7 +26,7 @@
     "notifications_attachment_link_expired": "el enlace de descarga ha expirado",
     "notifications_click_copy_url_title": "Copiar la URL del enlace en el portapapeles",
     "notifications_click_copy_url_button": "Copiar enlace",
-    "notifications_click_open_title": "Ir a {{url}}",
+    "notifications_actions_open_url_title": "Ir a {{url}}",
     "notifications_click_open_button": "Abrir enlace",
     "notifications_none_for_topic_title": "Aún no has recibido ninguna notificación en este tópico.",
     "notifications_none_for_topic_description": "Para enviar notificaciones a este tópico, simplemente realice un PUT o POST a la URL del tópico.",
diff --git a/web/public/static/langs/fr.json b/web/public/static/langs/fr.json
index b83cf869..bb559af2 100644
--- a/web/public/static/langs/fr.json
+++ b/web/public/static/langs/fr.json
@@ -24,7 +24,7 @@
     "notifications_click_copy_url_button": "Copier le lien",
     "notifications_click_open_button": "Ouvrir le lien",
     "notifications_none_for_topic_title": "Vous n'avez pas encore reçu de notifications pour ce sujet.",
-    "notifications_click_open_title": "Aller à {{url}}",
+    "notifications_actions_open_url_title": "Aller à {{url}}",
     "notifications_example": "Exemple",
     "notifications_loading": "Chargement des notifications…",
     "publish_dialog_progress_uploading": "Téléversement…",
diff --git a/web/public/static/langs/id.json b/web/public/static/langs/id.json
index 2b4c1192..b9dfde1b 100644
--- a/web/public/static/langs/id.json
+++ b/web/public/static/langs/id.json
@@ -29,7 +29,7 @@
     "notifications_attachment_open_button": "Buka lampiran",
     "notifications_attachment_link_expires": "tautan kadaluwarsa {{date}}",
     "notifications_attachment_link_expired": "tautan unduhan kadaluwarsa",
-    "notifications_click_open_title": "Pergi ke {{url}}",
+    "notifications_actions_open_url_title": "Pergi ke {{url}}",
     "notifications_click_open_button": "Buka tautan",
     "publish_dialog_topic_placeholder": "Nama topik, mis. pemberitahuan_andi",
     "nav_button_publish_message": "Publikasikan notifikasi",
diff --git a/web/public/static/langs/ja.json b/web/public/static/langs/ja.json
index e456a655..e9d51cbc 100644
--- a/web/public/static/langs/ja.json
+++ b/web/public/static/langs/ja.json
@@ -40,7 +40,7 @@
     "notifications_attachment_copy_url_button": "URLをコピー",
     "notifications_attachment_open_title": "{{url}} に移動",
     "notifications_attachment_link_expired": "ダウンロードリンクは失効しました",
-    "notifications_click_open_title": "{{url}} に移動",
+    "notifications_actions_open_url_title": "{{url}} に移動",
     "notifications_attachment_copy_url_title": "添付URLをクリップボードにコピー",
     "notifications_attachment_open_button": "添付ファイルを開く",
     "notifications_click_copy_url_title": "リンクURLをクリップボードにコピー",
diff --git a/web/public/static/langs/nb_NO.json b/web/public/static/langs/nb_NO.json
index 84526e64..1dab51b3 100644
--- a/web/public/static/langs/nb_NO.json
+++ b/web/public/static/langs/nb_NO.json
@@ -18,7 +18,7 @@
     "notifications_attachment_open_title": "Gå til {{url}}",
     "notifications_attachment_link_expires": "lenken utløper {{date}}",
     "notifications_click_copy_url_title": "Kopier lenke-nettadresse til utklippstavlen",
-    "notifications_click_open_title": "Gå til {{url}}",
+    "notifications_actions_open_url_title": "Gå til {{url}}",
     "notifications_tags": "Etiketter",
     "notifications_attachment_link_expired": "nedlastingslenken har utløpt",
     "notifications_none_for_any_title": "Du har ikke mottatt noen merknader.",
diff --git a/web/public/static/langs/ru.json b/web/public/static/langs/ru.json
index 5f4c4e69..ed97047a 100644
--- a/web/public/static/langs/ru.json
+++ b/web/public/static/langs/ru.json
@@ -59,7 +59,7 @@
     "notifications_none_for_any_title": "Вы ещё не получали никаких уведомлений.",
     "alert_grant_title": "Уведомления отключены",
     "notifications_attachment_copy_url_title": "Скопировать URL-адрес вложения",
-    "notifications_click_open_title": "Перейти на {{url}}",
+    "notifications_actions_open_url_title": "Перейти на {{url}}",
     "notifications_tags": "Тэги",
     "notifications_attachment_link_expires": "срок действия ссылки истекает {{date}}",
     "notifications_click_copy_url_title": "Скопировать URL-адрес ссылки",
diff --git a/web/public/static/langs/tr.json b/web/public/static/langs/tr.json
index 24250059..254213ef 100644
--- a/web/public/static/langs/tr.json
+++ b/web/public/static/langs/tr.json
@@ -71,7 +71,7 @@
     "notifications_none_for_any_title": "Herhangi bir bildirim almadınız.",
     "notifications_attachment_link_expired": "indirme bağlantısının süresi doldu",
     "notifications_click_copy_url_button": "Bağlantıyı kopyala",
-    "notifications_click_open_title": "{{url}} adresine git",
+    "notifications_actions_open_url_title": "{{url}} adresine git",
     "notifications_click_open_button": "Bağlantıyı aç",
     "notifications_no_subscriptions_description": "Bir konu oluşturmak veya bir konuya abone olmak için \"{{linktext}}\" bağlantısına tıklayın. Bundan sonra PUT veya POST yoluyla mesaj gönderebilirsiniz ve buradan bildirimler alırsınız.",
     "notifications_example": "Örnek",
diff --git a/web/src/app/SubscriptionManager.js b/web/src/app/SubscriptionManager.js
index b1e44498..b485383d 100644
--- a/web/src/app/SubscriptionManager.js
+++ b/web/src/app/SubscriptionManager.js
@@ -92,6 +92,19 @@ class SubscriptionManager {
         });
     }
 
+    async updateNotification(notification) {
+        const exists = await db.notifications.get(notification.id);
+        if (!exists) {
+            return false;
+        }
+        try {
+            await db.notifications.put({ ...notification });
+        } catch (e) {
+            console.error(`[SubscriptionManager] Error updating notification`, e);
+        }
+        return true;
+    }
+
     async deleteNotification(notificationId) {
         await db.notifications.delete(notificationId);
     }
diff --git a/web/src/app/utils.js b/web/src/app/utils.js
index adba2e9f..aaf89111 100644
--- a/web/src/app/utils.js
+++ b/web/src/app/utils.js
@@ -105,6 +105,18 @@ export const encodeBase64Url = (s) => {
     return Base64.encodeURI(s);
 }
 
+export const maybeAppendActionErrors = (message, notification) => {
+    const actionErrors = (notification.actions ?? [])
+        .map(action => action.error)
+        .filter(action => !!action)
+        .join("\n")
+    if (actionErrors.length === 0) {
+        return message;
+    } else {
+        return `${message}\n\n${actionErrors}`;
+    }
+}
+
 export const shuffle = (arr) => {
     let j, x;
     for (let index = arr.length - 1; index > 0; index--) {
diff --git a/web/src/components/Notifications.js b/web/src/components/Notifications.js
index a412e29e..3a952190 100644
--- a/web/src/components/Notifications.js
+++ b/web/src/components/Notifications.js
@@ -19,7 +19,7 @@ import {
     formatBytes,
     formatMessage,
     formatShortDateTime,
-    formatTitle,
+    formatTitle, maybeAppendActionErrors,
     openUrl,
     shortUrl,
     topicShortUrl,
@@ -138,9 +138,10 @@ const NotificationItem = (props) => {
         props.onShowSnack();
     };
     const expired = attachment && attachment.expires && attachment.expires < Date.now()/1000;
-    const showAttachmentActions = attachment && !expired;
-    const showClickAction = notification.click;
-    const showActions = showAttachmentActions || showClickAction;
+    const hasAttachmentActions = attachment && !expired;
+    const hasClickAction = notification.click;
+    const hasUserActions = notification.actions && notification.actions.length > 0;
+    const showActions = hasAttachmentActions || hasClickAction || hasUserActions;
     return (
         <Card sx={{ minWidth: 275, padding: 1 }}>
             <CardContent>
@@ -161,13 +162,15 @@ const NotificationItem = (props) => {
                         </svg>}
                 </Typography>
                 {notification.title && <Typography variant="h5" component="div">{formatTitle(notification)}</Typography>}
-                <Typography variant="body1" sx={{ whiteSpace: 'pre-line' }}>{autolink(formatMessage(notification))}</Typography>
+                <Typography variant="body1" sx={{ whiteSpace: 'pre-line' }}>
+                    {autolink(maybeAppendActionErrors(formatMessage(notification), notification))}
+                </Typography>
                 {attachment && <Attachment attachment={attachment}/>}
                 {tags && <Typography sx={{ fontSize: 14 }} color="text.secondary">{t("notifications_tags")}: {tags}</Typography>}
             </CardContent>
             {showActions &&
                 <CardActions sx={{paddingTop: 0}}>
-                    {showAttachmentActions && <>
+                    {hasAttachmentActions && <>
                         <Tooltip title={t("notifications_attachment_copy_url_title")}>
                             <Button onClick={() => handleCopy(attachment.url)}>{t("notifications_attachment_copy_url_button")}</Button>
                         </Tooltip>
@@ -175,14 +178,15 @@ const NotificationItem = (props) => {
                             <Button onClick={() => openUrl(attachment.url)}>{t("notifications_attachment_open_button")}</Button>
                         </Tooltip>
                     </>}
-                    {showClickAction && <>
+                    {hasClickAction && <>
                         <Tooltip title={t("notifications_click_copy_url_title")}>
                             <Button onClick={() => handleCopy(notification.click)}>{t("notifications_click_copy_url_button")}</Button>
                         </Tooltip>
-                        <Tooltip title={t("notifications_click_open_title", { url: notification.click })}>
+                        <Tooltip title={t("notifications_actions_open_url_title", { url: notification.click })}>
                             <Button onClick={() => openUrl(notification.click)}>{t("notifications_click_open_button")}</Button>
                         </Tooltip>
                     </>}
+                    {hasUserActions && <UserActions notification={notification}/>}
                 </CardActions>}
         </Card>
     );
@@ -329,6 +333,83 @@ const Image = (props) => {
     );
 }
 
+const UserActions = (props) => {
+    return (
+        <>{props.notification.actions.map(action =>
+            <UserAction key={action.id} notification={props.notification} action={action}/>)}</>
+    );
+};
+
+const UserAction = (props) => {
+    const { t } = useTranslation();
+    const notification = props.notification;
+    const action = props.action;
+    if (action.action === "broadcast") {
+        return (
+            <Tooltip title={t("notifications_actions_not_supported")}>
+                <span><Button disabled>{action.label}</Button></span>
+            </Tooltip>
+        );
+    } else if (action.action === "view") {
+        return (
+            <Tooltip title={t("notifications_actions_open_url_title", { url: action.url })}>
+                <Button onClick={() => openUrl(action.url)}>{action.label}</Button>
+            </Tooltip>
+        );
+    } else if (action.action === "http") {
+        const method = action.method ?? "POST";
+        const label = action.label + (ACTION_LABEL_SUFFIX[action.progress ?? 0] ?? "");
+        return (
+            <Tooltip title={t("notifications_actions_http_request_title", { method: method, url: action.url })}>
+                <Button onClick={() => performHttpAction(notification, action)}>{label}</Button>
+            </Tooltip>
+        );
+    }
+    return null; // Others
+};
+
+const performHttpAction = async (notification, action) => {
+    console.log(`[Notifications] Performing HTTP user action`, action);
+    try {
+        updateActionStatus(notification, action, ACTION_PROGRESS_ONGOING, null);
+        const response = await fetch(action.url, {
+            method: action.method ?? "POST",
+            headers: action.headers ?? {},
+            body: action.body ?? ""
+        });
+        console.log(`[Notifications] HTTP user action response`, response);
+        const success = response.status >= 200 && response.status <= 299;
+        if (success) {
+            updateActionStatus(notification, action, ACTION_PROGRESS_SUCCESS, null);
+        } else {
+            updateActionStatus(notification, action, ACTION_PROGRESS_FAILED, `${action.label}: Unexpected response HTTP ${response.status}`);
+        }
+    } catch (e) {
+        console.log(`[Notifications] HTTP action failed`, e);
+        updateActionStatus(notification, action, ACTION_PROGRESS_FAILED, `${action.label}: ${e} Check developer console for details.`);
+    }
+};
+
+const updateActionStatus = (notification, action, progress, error) => {
+    notification.actions = notification.actions.map(a => {
+        if (a.id !== action.id) {
+            return a;
+        }
+        return { ...a, progress: progress, error: error };
+    });
+    subscriptionManager.updateNotification(notification);
+}
+
+const ACTION_PROGRESS_ONGOING = 1;
+const ACTION_PROGRESS_SUCCESS = 2;
+const ACTION_PROGRESS_FAILED = 3;
+
+const ACTION_LABEL_SUFFIX = {
+    [ACTION_PROGRESS_ONGOING]: " …",
+    [ACTION_PROGRESS_SUCCESS]: " ✔",
+    [ACTION_PROGRESS_FAILED]: " ❌"
+};
+
 const NoNotifications = (props) => {
     const { t } = useTranslation();
     const shortUrl = topicShortUrl(props.subscription.baseUrl, props.subscription.topic);