diff --git a/Makefile b/Makefile
index 1894075ce50d13806abc1448cf0735a65f6df78a..a5513bad343a34627993b501de879b8c6628b690 100644
--- a/Makefile
+++ b/Makefile
@@ -206,7 +206,7 @@ release-check-tags:
 # Installing targets
 
 install-amd64: remove-binary
-	sudo cp -a dist/ntfy_amd64_linux_amd64/ntfy /usr/bin/ntfy
+	sudo cp -a dist/ntfy_amd64_linux_amd64_v1/ntfy /usr/bin/ntfy
 
 install-armv6: remove-binary
 	sudo cp -a dist/ntfy_armv6_linux_armv6/ntfy /usr/bin/ntfy
diff --git a/client/options.go b/client/options.go
index 1d3b7ac2c36465a5108556c5ddcceec3c9f6f4af..7d5996994f84c413e26676162bc7074f7759f5aa 100644
--- a/client/options.go
+++ b/client/options.go
@@ -56,6 +56,12 @@ func WithClick(url string) PublishOption {
 	return WithHeader("X-Click", url)
 }
 
+// WithActions adds custom user actions to the notification. The value can be either a JSON array or the
+// simple format definition. See https://ntfy.sh/docs/publish/#action-buttons for details.
+func WithActions(value string) PublishOption {
+	return WithHeader("X-Actions", value)
+}
+
 // WithAttach sets a URL that will be used by the client to download an attachment
 func WithAttach(attach string) PublishOption {
 	return WithHeader("X-Attach", attach)
diff --git a/cmd/publish.go b/cmd/publish.go
index 6a4c7f0bee742ee7018370ee1a3f400fc036491e..e210308a67d75cda9ec2c6128b71036f946b89d6 100644
--- a/cmd/publish.go
+++ b/cmd/publish.go
@@ -26,6 +26,7 @@ var cmdPublish = &cli.Command{
 		&cli.StringFlag{Name: "tags", Aliases: []string{"tag", "T"}, EnvVars: []string{"NTFY_TAGS"}, Usage: "comma separated list of tags and emojis"},
 		&cli.StringFlag{Name: "delay", Aliases: []string{"at", "in", "D"}, EnvVars: []string{"NTFY_DELAY"}, Usage: "delay/schedule message"},
 		&cli.StringFlag{Name: "click", Aliases: []string{"U"}, EnvVars: []string{"NTFY_CLICK"}, Usage: "URL to open when notification is clicked"},
+		&cli.StringFlag{Name: "actions", Aliases: []string{"A"}, EnvVars: []string{"NTFY_ACTIONS"}, Usage: "actions JSON array or simple definition"},
 		&cli.StringFlag{Name: "attach", Aliases: []string{"a"}, EnvVars: []string{"NTFY_ATTACH"}, Usage: "URL to send as an external attachment"},
 		&cli.StringFlag{Name: "filename", Aliases: []string{"name", "n"}, EnvVars: []string{"NTFY_FILENAME"}, Usage: "filename for the attachment"},
 		&cli.StringFlag{Name: "file", Aliases: []string{"f"}, EnvVars: []string{"NTFY_FILE"}, Usage: "file to upload as an attachment"},
@@ -72,6 +73,7 @@ func execPublish(c *cli.Context) error {
 	tags := c.String("tags")
 	delay := c.String("delay")
 	click := c.String("click")
+	actions := c.String("actions")
 	attach := c.String("attach")
 	filename := c.String("filename")
 	file := c.String("file")
@@ -112,6 +114,9 @@ func execPublish(c *cli.Context) error {
 	if click != "" {
 		options = append(options, client.WithClick(click))
 	}
+	if actions != "" {
+		options = append(options, client.WithActions(strings.ReplaceAll(actions, "\n", " ")))
+	}
 	if attach != "" {
 		options = append(options, client.WithAttach(attach))
 	}
diff --git a/docs/publish.md b/docs/publish.md
index 54d2642c548ff175f760d8a2121af55a0696e753..2948c4e4a9938087677b7c80af04193354b3664a 100644
--- a/docs/publish.md
+++ b/docs/publish.md
@@ -789,21 +789,21 @@ The JSON message format closely mirrors the format of the message you can consum
 (see [JSON message format](subscribe/api.md#json-message-format) for details), but is not exactly identical. Here's an overview of
 all the supported fields:
 
-| Field      | Required | Type                             | Example                               | Description                                                           |
-|------------|----------|----------------------------------|---------------------------------------|-----------------------------------------------------------------------|
-| `topic`    | ✔️       | *string*                         | `topic1`                              | Target topic name                                                     |
-| `message`  | -        | *string*                         | `Some message`                        | Message body; set to `triggered` if empty or not passed               |
-| `title`    | -        | *string*                         | `Some title`                          | Message [title](#message-title)                                       |
-| `tags`     | -        | *string array*                   | `["tag1","tag2"]`                     | List of [tags](#tags-emojis) that may or not map to emojis            |
-| `priority` | -        | *int (one of: 1, 2, 3, 4, or 5)* | `4`                                   | Message [priority](#message-priority) with 1=min, 3=default and 5=max |
-| `actions`  | -        | *JSON array*                     | *(see [user actions](#user-actions))* | Custom [user action buttons](#user-actions) for notifications         |
-| `click`    | -        | *URL*                            | `https://example.com`                 | Website opened when notification is [clicked](#click-action)          |
-| `attach`   | -        | *URL*                            | `https://example.com/file.jpg`        | URL of an attachment, see [attach via URL](#attach-file-from-url)     |
-| `filename` | -        | *string*                         | `file.jpg`                            | File name of the attachment                                           |
-| `delay`    | -        | *string*                         | `30min`, `9am`                        | Timestamp or duration for delayed delivery                            |
-| `email`    | -        | *e-mail address*                 | `phil@example.com`                    | E-mail address for e-mail notifications                               |
-
-## User actions
+| Field      | Required | Type                             | Example                                   | Description                                                           |
+|------------|----------|----------------------------------|-------------------------------------------|-----------------------------------------------------------------------|
+| `topic`    | ✔️       | *string*                         | `topic1`                                  | Target topic name                                                     |
+| `message`  | -        | *string*                         | `Some message`                            | Message body; set to `triggered` if empty or not passed               |
+| `title`    | -        | *string*                         | `Some title`                              | Message [title](#message-title)                                       |
+| `tags`     | -        | *string array*                   | `["tag1","tag2"]`                         | List of [tags](#tags-emojis) that may or not map to emojis            |
+| `priority` | -        | *int (one of: 1, 2, 3, 4, or 5)* | `4`                                       | Message [priority](#message-priority) with 1=min, 3=default and 5=max |
+| `actions`  | -        | *JSON array*                     | *(see [actiom buttons](#action-buttons))* | Custom [user action buttons](#action-buttons) for notifications       |
+| `click`    | -        | *URL*                            | `https://example.com`                     | Website opened when notification is [clicked](#click-action)          |
+| `attach`   | -        | *URL*                            | `https://example.com/file.jpg`            | URL of an attachment, see [attach via URL](#attach-file-from-url)     |
+| `filename` | -        | *string*                         | `file.jpg`                                | File name of the attachment                                           |
+| `delay`    | -        | *string*                         | `30min`, `9am`                            | Timestamp or duration for delayed delivery                            |
+| `email`    | -        | *e-mail address*                 | `phil@example.com`                        | E-mail address for e-mail notifications                               |
+
+## Action buttons
 You can add action buttons to notifications to allow yourself to react to a notification directly. This is incredibly
 useful and has countless applications. As of today, the following actions are supported:
 
@@ -812,6 +812,9 @@ useful and has countless applications. As of today, the following actions are su
   when the action button is tapped
 * [`http`](#send-http-request): Sends HTTP POST/GET/PUT request when the action button is tapped
 
+To define the user actions, you can either pass the `actions` field as part of the JSON body (if you're 
+[publishing via JSON](#publish-as-json)), or use the `X-Actions` header (or any of its aliases: `Actions`, `Action`).
+
 Here's an example of what that a notification with actions can look like:
 
 <figure markdown>
@@ -819,12 +822,91 @@ Here's an example of what that a notification with actions can look like:
   <figcaption>XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX</figcaption>
 </figure>
 
+Using the `X-Actions` header and the **simple format** (details see below), you can create the above notification like 
+this. This format is much easier to write, but less powerful:
+
+=== "Command line (curl)"
+    ```
+    curl \
+        -d "You left the house. Turn down the A/C?" \
+        -H "Actions: view, Open portal, https://home.nest.com/; \
+                     http, Turn down, https://api.nest.com/device/XZ1D2, body=target_temp_f=65" \
+    ntfy.sh/myhome
+    ```
+
+=== "ntfy CLI"
+    ```
+    ntfy publish \
+        --actions="view, Open portal, https://home.nest.com/; \
+                  http, Turn down, https://api.nest.com/device/XZ1D2, body=target_temp_f=65" \
+        myhome \
+        "You left the house. Turn down the A/C?"
+    ```
+
+=== "HTTP"
+    ``` http
+    POST /myhome HTTP/1.1
+    Host: ntfy.sh
+    Actions: view, Open portal, https://home.nest.com/; http, Turn down, https://api.nest.com/device/XZ1D2, body=target_temp_f=65
+
+    You left the house. Turn down the A/C?
+    ```
+
+=== "JavaScript"
+    ``` javascript
+    fetch('https://ntfy.sh/myhome', {
+        method: 'POST',
+        body: 'You left the house. Turn down the A/C?',
+        headers: { 
+            'Actions': 'view, Open portal, https://home.nest.com/; http, Turn down, https://api.nest.com/device/XZ1D2, body=target_temp_f=65' 
+        }
+    })
+    ```
+
+=== "Go"
+    ``` go
+    req, _ := http.NewRequest("POST", "https://ntfy.sh/myhome", strings.NewReader("You left the house. Turn down the A/C?"))
+    req.Header.Set("Actions", "view, Open portal, https://home.nest.com/; http, Turn down, https://api.nest.com/device/XZ1D2, body=target_temp_f=65")
+    http.DefaultClient.Do(req)
+    ```
+
+=== "PowerShell"
+    ``` powershell
+    $uri = "https://ntfy.sh/myhome"
+    $headers = @{ Actions="view, Open portal, https://home.nest.com/; http, Turn down, https://api.nest.com/device/XZ1D2, body=target_temp_f=65" }
+    $body = "You left the house. Turn down the A/C?"
+    Invoke-RestMethod -Method 'Post' -Uri $uri -Headers $headers -Body $body -UseBasicParsing
+    ```
+
+=== "Python"
+    ``` python
+    requests.post("https://ntfy.sh/myhome",
+        data="You left the house. Turn down the A/C?",
+        headers={ "Actions": "view, Open portal, https://home.nest.com/; http, Turn down, https://api.nest.com/device/XZ1D2, body=target_temp_f=65" })
+    ```
+
+=== "PHP"
+    ``` php-inline
+    file_get_contents('https://ntfy.sh/reddit_alerts', false, stream_context_create([
+        'http' => [
+            'method' => 'POST',
+            'header' =>
+                "Content-Type: text/plain\r\n" .
+                "Actions: view, Open portal, https://home.nest.com/; http, Turn down, https://api.nest.com/device/XZ1D2, body=target_temp_f=65",
+            'content' => 'You left the house. Turn down the A/C?'
+        ]
+    ]));
+    ```
+
+Alternatively, you can define actions as **JSON array** (details see below), and pass them as part of the JSON body 
+(see [publish as JSON](#publish-as-json)):
+
 === "Command line (curl)"
     ```
     curl ntfy.sh \
       -d '{
         "topic": "myhome",
-        "message": "You seem to have left the house. Want to turn down the A/C?",
+        "message": "You left the house. Turn down the A/C?",
         "actions": [
           {
             "action": "view",
@@ -834,39 +916,54 @@ Here's an example of what that a notification with actions can look like:
           {
             "action": "http",
             "label": "Turn down",
-            "method": "POST",
-            "url": "https://developer-api.nest.com/devices/thermostats/XZA124D",
-            "headers": {
-              "Authorization": "Bearer ...",
-              "Content-Type": "application/json"
-            },
-            "body": "{\"target_temperature_f\": 65}"
-          },
-          { 
-            "action": "broadcast", 
-            "label": "Enter deep sleep 💤",
-            "extras": {
-              "command": "deepsleep"
-            }
+            "url": "https://api.nest.com/device/XZ1D2",
+            "body": "target_temp_f=65"
           }
         ]
       }'
     ```
 
+=== "ntfy CLI"
+    ```
+    ntfy publish \
+        --actions '[
+            {
+                "action": "view",
+                "label": "Open portal",
+                "url": "https://home.nest.com/"
+            },
+            {
+                "action": "http",
+                "label": "Turn down",
+                "url": "https://api.nest.com/device/XZ1D2",
+                "body": "target_temp_f=65"
+            }
+        ]' \
+        myhome \
+        "You left the house. Turn down the A/C?"
+    ```
+
 === "HTTP"
     ``` http
     POST / HTTP/1.1
     Host: ntfy.sh
 
     {
-        "topic": "mytopic",
-        "message": "Disk space is low at 5.1 GB",
-        "title": "Low disk space alert",
-        "tags": ["warning","cd"],
-        "priority": 4,
-        "attach": "https://filesrv.lan/space.jpg",
-        "filename": "diskspace.jpg",
-        "click": "https://homecamera.lan/xasds1h2xsSsa/"
+        "topic": "myhome",
+        "message": "You left the house. Turn down the A/C?",
+        "actions": [
+          {
+            "action": "view",
+            "label": "Open portal",
+            "url": "https://home.nest.com/"
+          },
+          {
+            "action": "http",
+            "label": "Turn down",
+            "url": "https://api.nest.com/device/XZ1D2",
+            "body": "target_temp_f=65"
+          }
+        ]
     }
     ```
 
@@ -875,14 +972,21 @@ Here's an example of what that a notification with actions can look like:
     fetch('https://ntfy.sh', {
         method: 'POST',
         body: JSON.stringify({
-            "topic": "mytopic",
-            "message": "Disk space is low at 5.1 GB",
-            "title": "Low disk space alert",
-            "tags": ["warning","cd"],
-            "priority": 4,
-            "attach": "https://filesrv.lan/space.jpg",
-            "filename": "diskspace.jpg",
-            "click": "https://homecamera.lan/xasds1h2xsSsa/"
+            topic: "myhome",
+            message": "You left the house. Turn down the A/C?",
+            actions: [
+                {
+                    action: "view",
+                    label: "Open portal",
+                    url: "https://home.nest.com/"
+                },
+                {
+                    action: "http",
+                    label: "Turn down",
+                    url: "https://api.nest.com/device/XZ1D2",
+                    body: "target_temp_f=65"
+                }
+            ]
         })
     })
     ```
@@ -890,18 +994,24 @@ Here's an example of what that a notification with actions can look like:
 === "Go"
     ``` go
     // You should probably use json.Marshal() instead and make a proper struct,
-    // or even just use req.Header.Set() like in the other examples, but for the 
-    // sake of the example, this is easier.
+    // but for the sake of the example, this is easier.
     
     body := `{
-        "topic": "mytopic",
-        "message": "Disk space is low at 5.1 GB",
-        "title": "Low disk space alert",
-        "tags": ["warning","cd"],
-        "priority": 4,
-        "attach": "https://filesrv.lan/space.jpg",
-        "filename": "diskspace.jpg",
-        "click": "https://homecamera.lan/xasds1h2xsSsa/"
+        "topic": "myhome",
+        "message": "You left the house. Turn down the A/C?",
+        "actions": [
+          {
+            "action": "view",
+            "label": "Open portal",
+            "url": "https://home.nest.com/"
+          },
+          {
+            "action": "http",
+            "label": "Turn down",
+            "url": "https://api.nest.com/device/XZ1D2",
+            "body": "target_temp_f=65"
+          }
+        ]
     }`
     req, _ := http.NewRequest("POST", "https://ntfy.sh/", strings.NewReader(body))
     http.DefaultClient.Do(req)
@@ -911,15 +1021,22 @@ Here's an example of what that a notification with actions can look like:
     ``` powershell
     $uri = "https://ntfy.sh"
     $body = @{
-            "topic"="powershell"
-            "title"="Low disk space alert"
-            "message"="Disk space is low at 5.1 GB"
-            "priority"=4
-            "attach"="https://filesrv.lan/space.jpg"
-            "filename"="diskspace.jpg"
-            "tags"=@("warning","cd")
-            "click"= "https://homecamera.lan/xasds1h2xsSsa/"
-          } | ConvertTo-Json
+        "topic"="myhome"
+        "message"="You left the house. Turn down the A/C?"
+        "actions"=@(
+            @{
+                "action"="view"
+                "label"="Open portal"
+                "url"="https://home.nest.com/"
+            },
+            @{
+                "action"="http",
+                "label"="Turn down"
+                "url"="https://api.nest.com/device/XZ1D2"
+                "body"="target_temp_f=65"
+            }
+        )
+    } | ConvertTo-Json
     Invoke-RestMethod -Method 'Post' -Uri $uri -Body $body -ContentType "application/json" -UseBasicParsing
     ```
 
@@ -927,14 +1044,21 @@ Here's an example of what that a notification with actions can look like:
     ``` python
     requests.post("https://ntfy.sh/",
         data=json.dumps({
-            "topic": "mytopic",
-            "message": "Disk space is low at 5.1 GB",
-            "title": "Low disk space alert",
-            "tags": ["warning","cd"],
-            "priority": 4,
-            "attach": "https://filesrv.lan/space.jpg",
-            "filename": "diskspace.jpg",
-            "click": "https://homecamera.lan/xasds1h2xsSsa/"
+            "topic": "myhome",
+            "message": "You left the house. Turn down the A/C?",
+            "actions": [
+                {
+                    "action": "view",
+                    "label": "Open portal",
+                    "url": "https://home.nest.com/"
+                },
+                {
+                    "action": "http",
+                    "label": "Turn down",
+                    "url": "https://api.nest.com/device/XZ1D2",
+                    "body": "target_temp_f=65"
+                }
+            ]
         })
     )
     ```
@@ -946,31 +1070,57 @@ Here's an example of what that a notification with actions can look like:
             'method' => 'POST',
             'header' => "Content-Type: application/json",
             'content' => json_encode([
-                "topic": "mytopic",
-                "message": "Disk space is low at 5.1 GB",
-                "title": "Low disk space alert",
-                "tags": ["warning","cd"],
-                "priority": 4,
-                "attach": "https://filesrv.lan/space.jpg",
-                "filename": "diskspace.jpg",
-                "click": "https://homecamera.lan/xasds1h2xsSsa/"
+                "topic": "myhome",
+                "message": "You left the house. Turn down the A/C?",
+                "actions": [
+                    [
+                        "action": "view",
+                        "label": "Open portal",
+                        "url": "https://home.nest.com/"
+                    ],
+                    [
+                        "action": "http",
+                        "label": "Turn down",
+                        "url": "https://api.nest.com/device/XZ1D2",
+                        "headers": [
+                            "Authorization": "Bearer ..."
+                        ],
+                        "body": "target_temp_f=65"
+                    ]
+                ]
             ])
         ]
     ]));
     ```
 
+**Simple format syntax:**
+
+Generally, the `X-Actions` header is formatted like this:
+```
+Actions: <action>, <label>, <params, ...>
+```
+or:
+```
+Actions: action=<action>, label=<label>, param1=..., param2=..., ...
+```
+
+An `action` is either [`view`](#open-websiteapp), [`broadcast`](#send-android-broadcast), or [`http`](#send-http-request),
+and the `label` defines the button text. The other parameters depend on the action itself.
 
 | Field    | Required | Type                       | Example         | Description                                    |
 |----------|----------|----------------------------|-----------------|------------------------------------------------|
 | `action` | ✔️       | *view, broadcast, or http* | `view`          | Action type                                    |
 | `label`  | ✔️        | *string*                   | `Turn on light` | Label of the action button in the notification |
-
+XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXx
 
 
 ### Open website/app
 The `view` action opens a website or app when the action button is tapped, e.g. a browser, a Google Maps location, or
 even a deep link into Twitter or a show ntfy topic.
 
+XXXXXXXXXXXXXXXXXXx
+
+
 ### Send Android broadcast
 The `broadcast` action sends an [Android broadcast](https://developer.android.com/guide/components/broadcasts) intent
 when the action button is tapped. This allows integration into automation apps such as [MacroDroid](https://play.google.com/store/apps/details?id=com.arlosoft.macrodroid)
@@ -978,10 +1128,15 @@ or [Tasker](https://play.google.com/store/apps/details?id=net.dinglisch.android.
 you can do everything your phone is capable of. Examples include taking pictures, launching/killing apps, change device
 settings, write/read files, etc.
 
+XXXXXXXXXXXXXXxx
+
+
 ### Send HTTP request
 The `http` action sends a HTTP POST/GET/PUT request when the action button is tapped. You can use this to trigger REST APIs
 for whatever systems you have, e.g. opening the garage door, or turning on/off lights.
 
+XXXXXXXXXXXXXXXXXXXXx
+
 === "`view` action"
     ``` json
     { 
@@ -1079,6 +1234,18 @@ You can define which URL to open when a notification is clicked. This may be use
 to a Zabbix alert or a transaction that you'd like to provide the deep-link for. Tapping the notification will open
 the web browser (or the app) and open the website.
 
+To define a click action for the notification, pass a URL as the value of the `X-Click` header (or its aliase `Click`).
+If you pass a website URL (`http://` or `https://`) the web browser will open. If you pass another URI that can be handled
+by another app, the responsible app may open. 
+
+Examples:
+
+* `http://` or `https://` will open your browser (or an app if it registered for a URL)
+* `mailto:` links will open your mail app
+* `geo:` links will open Google Maps (or your maps application)
+* `ntfy://` links will open ntfy (see [ntfy:// links](subscribe/phone.md#ntfy-links))
+* ...
+
 Here's an example that will open Reddit when the notification is clicked:
 
 === "Command line (curl)"
@@ -1751,7 +1918,7 @@ and can be passed as **HTTP headers** or **query parameters in the URL**. They a
 | `X-Priority`    | `Priority`, `prio`, `p`                    | [Message priority](#message-priority)                                                         |
 | `X-Tags`        | `Tags`, `Tag`, `ta`                        | [Tags and emojis](#tags-emojis)                                                               |
 | `X-Delay`       | `Delay`, `X-At`, `At`, `X-In`, `In`        | Timestamp or duration for [delayed delivery](#scheduled-delivery)                             |
-| `X-Actions`     | `Actions`, `Action`                        | JSON array or short format of [user actions](#user-actions)                                   |
+| `X-Actions`     | `Actions`, `Action`                        | JSON array or short format of [user actions](#action-buttons)                                 |
 | `X-Click`       | `Click`                                    | URL to open when [notification is clicked](#click-action)                                     |
 | `X-Attach`      | `Attach`, `a`                              | URL to send as an [attachment](#attachments), as an alternative to PUT/POST-ing an attachment |
 | `X-Filename`    | `Filename`, `file`, `f`                    | Optional [attachment](#attachments) filename, as it appears in the client                     |
diff --git a/go.mod b/go.mod
index 2bb1b7d2f13054bb0deadff31ca7100d967ebbc2..acb27e98e515e08e57cc45c34703e2fbbb1b31b3 100644
--- a/go.mod
+++ b/go.mod
@@ -24,6 +24,8 @@ require (
 	gopkg.in/yaml.v2 v2.4.0
 )
 
+require github.com/pkg/errors v0.9.1
+
 require (
 	cloud.google.com/go v0.100.2 // indirect
 	cloud.google.com/go/compute v1.5.0 // indirect
@@ -35,7 +37,6 @@ require (
 	github.com/golang/protobuf v1.5.2 // indirect
 	github.com/google/go-cmp v0.5.7 // indirect
 	github.com/googleapis/gax-go/v2 v2.2.0 // indirect
-	github.com/pkg/errors v0.9.1 // indirect
 	github.com/pmezard/go-difflib v1.0.0 // indirect
 	github.com/russross/blackfriday/v2 v2.1.0 // indirect
 	go.opencensus.io v0.23.0 // indirect
diff --git a/server/errors.go b/server/errors.go
index a65b4115c7d6551da94e8e9b830a771d2f4b0647..45cc406cd3029204e15d34599fa90b47c78df9d1 100644
--- a/server/errors.go
+++ b/server/errors.go
@@ -39,7 +39,7 @@ var (
 	errHTTPBadRequestAttachmentsExpiryBeforeDelivery = &errHTTP{40015, http.StatusBadRequest, "invalid request: attachment expiry before delayed delivery date", "https://ntfy.sh/docs/publish/#scheduled-delivery"}
 	errHTTPBadRequestWebSocketsUpgradeHeaderMissing  = &errHTTP{40016, http.StatusBadRequest, "invalid request: client not using the websocket protocol", "https://ntfy.sh/docs/subscribe/api/#websockets"}
 	errHTTPBadRequestJSONInvalid                     = &errHTTP{40017, http.StatusBadRequest, "invalid request: request body must be message JSON", "https://ntfy.sh/docs/publish/#publish-as-json"}
-	errHTTPBadRequestActionsInvalid                  = &errHTTP{40018, http.StatusBadRequest, "invalid request: actions are invalid format", "https://ntfy.sh/docs/publish/#user-actions"}
+	errHTTPBadRequestActionsInvalid                  = &errHTTP{40018, http.StatusBadRequest, "invalid request: actions are invalid format", "https://ntfy.sh/docs/publish/#action-buttons"}
 	errHTTPNotFound                                  = &errHTTP{40401, http.StatusNotFound, "page not found", ""}
 	errHTTPUnauthorized                              = &errHTTP{40101, http.StatusUnauthorized, "unauthorized", "https://ntfy.sh/docs/publish/#authentication"}
 	errHTTPForbidden                                 = &errHTTP{40301, http.StatusForbidden, "forbidden", "https://ntfy.sh/docs/publish/#authentication"}
diff --git a/server/server.go b/server/server.go
index 36a5f3307f6d14ef3674c6786adda342a57a0b07..ab897da86206ef381ee99f52624d2f340e7d7fb1 100644
--- a/server/server.go
+++ b/server/server.go
@@ -93,7 +93,6 @@ const (
 	emptyMessageBody         = "triggered"               // Used if message body is empty
 	defaultAttachmentMessage = "You received a file: %s" // Used if message body is empty, and there is an attachment
 	encodingBase64           = "base64"
-	actionIDLength           = 10
 )
 
 // WebSocket constants
diff --git a/server/util.go b/server/util.go
index 4b6ca03986709cb5c524898ff09ee3365e818b66..a6ad580a516d0581a4f7ecd47fbd47356e1f651c 100644
--- a/server/util.go
+++ b/server/util.go
@@ -9,6 +9,11 @@ import (
 	"strings"
 )
 
+const (
+	actionIDLength = 10
+	actionsMax     = 3
+)
+
 func readBoolParam(r *http.Request, defaultValue bool, names ...string) bool {
 	value := strings.ToLower(readParam(r, names...))
 	if value == "" {
@@ -63,12 +68,15 @@ func parseActions(s string) (actions []*action, err error) {
 	}
 
 	// Validate
+	if len(actions) > actionsMax {
+		return nil, fmt.Errorf("too many actions, only %d allowed", actionsMax)
+	}
 	for _, action := range actions {
 		if !util.InStringList([]string{"view", "broadcast", "http"}, action.Action) {
 			return nil, fmt.Errorf("cannot parse actions: action '%s' unknown", action.Action)
 		} else if action.Label == "" {
 			return nil, fmt.Errorf("cannot parse actions: label must be set")
-		} else if util.InStringList([]string{"view", "http"}, action.Action) && action.URL != "" {
+		} else if util.InStringList([]string{"view", "http"}, action.Action) && action.URL == "" {
 			return nil, fmt.Errorf("parameter 'url' is required for action '%s'", action.Action)
 		}
 	}
@@ -99,8 +107,8 @@ func parseActionsFromSimple(s string) ([]*action, error) {
 				newAction.Action = value
 			} else if key == "" && i == 1 {
 				newAction.Label = value
-			} else if key == "" && i == 2 {
-				newAction.URL = value // This works, because both "http" and "view" need a URL
+			} else if key == "" && util.InStringList([]string{"view", "http"}, newAction.Action) && i == 2 {
+				newAction.URL = value
 			} else if key != "" {
 				switch strings.ToLower(key) {
 				case "action":