diff --git a/docs/releases.md b/docs/releases.md
index 5e76bdc72384d5225547b2c131aa57d3523436cf..479e7c501e26d4bfcf1d82c83f5db128a7bc7cb5 100644
--- a/docs/releases.md
+++ b/docs/releases.md
@@ -26,6 +26,7 @@ and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/release
 **Features:**
 
 * Regularly send Firebase keepalive messages to ~poll topic to support self-hosted servers (no ticket)
+* Add subscribe filter to query exact messages by ID (no ticket) 
 
 **Bugs:**
 
diff --git a/docs/subscribe/api.md b/docs/subscribe/api.md
index 67d3458fdafea1480a0142ed74062425c45780c2..7eb3a95e7e9cb9e38966c5ac4082cb4fc460f0fe 100644
--- a/docs/subscribe/api.md
+++ b/docs/subscribe/api.md
@@ -267,7 +267,7 @@ curl -s "ntfy.sh/mytopic/json?poll=1&sched=1"
 ```
 
 ### Filter messages
-You can filter which messages are returned based on the well-known message fields `message`, `title`, `priority` and
+You can filter which messages are returned based on the well-known message fields `id`, `message`, `title`, `priority` and
 `tags`. Here's an example that only returns messages of high or urgent priority that contains the both tags 
 "zfs-error" and "error". Note that the `priority` filter is a logical OR and the `tags` filter is a logical AND. 
 
@@ -280,12 +280,13 @@ $ curl "ntfy.sh/alerts/json?priority=high&tags=zfs-error"
 
 Available filters (all case-insensitive):
 
-| Filter variable | Alias                     | Example                            | Description                                                             |
-|-----------------|---------------------------|------------------------------------|-------------------------------------------------------------------------|
-| `message`       | `X-Message`, `m`          | `ntfy.sh/mytopic?message=lalala`   | Only return messages that match this exact message string               |
-| `title`         | `X-Title`, `t`            | `ntfy.sh/mytopic?title=some+title` | Only return messages that match this exact title string                 |
-| `priority`      | `X-Priority`, `prio`, `p` | `ntfy.sh/mytopic?p=high,urgent`    | Only return messages that match *any priority listed* (comma-separated) |
-| `tags`          | `X-Tags`, `tag`, `ta`     | `ntfy.sh/mytopic?tags=error,alert` | Only return messages that match *all listed tags* (comma-separated)     |
+| Filter variable | Alias                     | Example                                       | Description                                                             |
+|-----------------|---------------------------|-----------------------------------------------|-------------------------------------------------------------------------|
+| `id`            | `X-ID`                    | `ntfy.sh/mytopic/json?poll=1&id=pbkiz8SD7ZxG` | Only return messages that match this exact message ID                   |
+| `message`       | `X-Message`, `m`          | `ntfy.sh/mytopic/json?message=lalala`         | Only return messages that match this exact message string               |
+| `title`         | `X-Title`, `t`            | `ntfy.sh/mytopic/json?title=some+title`       | Only return messages that match this exact title string                 |
+| `priority`      | `X-Priority`, `prio`, `p` | `ntfy.sh/mytopic/json?p=high,urgent`          | Only return messages that match *any priority listed* (comma-separated) |
+| `tags`          | `X-Tags`, `tag`, `ta`     | `ntfy.sh/mytopic?/jsontags=error,alert`       | Only return messages that match *all listed tags* (comma-separated)     |
 
 ### Subscribe to multiple topics
 It's possible to subscribe to multiple topics in one HTTP call by providing a comma-separated list of topics 
@@ -315,18 +316,19 @@ format of the message. It's very straight forward:
 
 **Message**:
 
-| Field        | Required | Type                                              | Example               | Description                                                                                                                          |
-|--------------|----------|---------------------------------------------------|-----------------------|--------------------------------------------------------------------------------------------------------------------------------------|
-| `id`         | ✔️       | *string*                                          | `hwQ2YpKdmg`          | Randomly chosen message identifier                                                                                                   |
-| `time`       | ✔️       | *number*                                          | `1635528741`          | Message date time, as Unix time stamp                                                                                                |  
-| `event`      | ✔️       | `open`, `keepalive`, `message`, or `poll_request` | `message`             | Message type, typically you'd be only interested in `message`                                                                        |
-| `topic`      | ✔️       | *string*                                          | `topic1,topic2`       | Comma-separated list of topics the message is associated with; only one for all `message` events, but may be a list in `open` events |
-| `message`    | -        | *string*                                          | `Some message`        | Message body; always present in `message` events                                                                                     |
-| `title`      | -        | *string*                                          | `Some title`          | Message [title](../publish.md#message-title); if not set defaults to `ntfy.sh/<topic>`                                               |
-| `tags`       | -        | *string array*                                    | `["tag1","tag2"]`     | List of [tags](../publish.md#tags-emojis) that may or not map to emojis                                                              |
-| `priority`   | -        | *1, 2, 3, 4, or 5*                                | `4`                   | Message [priority](../publish.md#message-priority) with 1=min, 3=default and 5=max                                                   |
-| `click`      | -        | *URL*                                             | `https://example.com` | Website opened when notification is [clicked](../publish.md#click-action)                                                            |
-| `attachment` | -        | *JSON object*                                     | *see below*           | Details about an attachment (name, URL, size, ...)                                                                                   |
+| Field        | Required | Type                                              | Example                                               | Description                                                                                                                          |
+|--------------|----------|---------------------------------------------------|-------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------|
+| `id`         | ✔️       | *string*                                          | `hwQ2YpKdmg`                                          | Randomly chosen message identifier                                                                                                   |
+| `time`       | ✔️       | *number*                                          | `1635528741`                                          | Message date time, as Unix time stamp                                                                                                |  
+| `event`      | ✔️       | `open`, `keepalive`, `message`, or `poll_request` | `message`                                             | Message type, typically you'd be only interested in `message`                                                                        |
+| `topic`      | ✔️       | *string*                                          | `topic1,topic2`                                       | Comma-separated list of topics the message is associated with; only one for all `message` events, but may be a list in `open` events |
+| `message`    | -        | *string*                                          | `Some message`                                        | Message body; always present in `message` events                                                                                     |
+| `title`      | -        | *string*                                          | `Some title`                                          | Message [title](../publish.md#message-title); if not set defaults to `ntfy.sh/<topic>`                                               |
+| `tags`       | -        | *string array*                                    | `["tag1","tag2"]`                                     | List of [tags](../publish.md#tags-emojis) that may or not map to emojis                                                              |
+| `priority`   | -        | *1, 2, 3, 4, or 5*                                | `4`                                                   | Message [priority](../publish.md#message-priority) with 1=min, 3=default and 5=max                                                   |
+| `click`      | -        | *URL*                                             | `https://example.com`                                 | Website opened when notification is [clicked](../publish.md#click-action)                                                            |
+| `actions`    | -        | *JSON array*                                      | *see [actions buttons](../publish.md#action-buttons)* | [Action buttons](../publish.md#action-buttons) that can be displayed in the notification                                             |
+| `attachment` | -        | *JSON object*                                     | *see below*                                           | Details about an attachment (name, URL, size, ...)                                                                                   |
 
 **Attachment** (part of the message, see [attachments](../publish.md#attachments) for details):
 
@@ -416,6 +418,7 @@ and can be passed as **HTTP headers** or **query parameters in the URL**. They a
 | `poll`      | `X-Poll`, `po`             | Return cached messages and close connection                                     |
 | `since`     | `X-Since`, `si`            | Return cached messages since timestamp, duration or message ID                  |
 | `scheduled` | `X-Scheduled`, `sched`     | Include scheduled/delayed messages in message list                              |
+| `id`        | `X-ID`                     | Filter: Only return messages that match this exact message ID                   |
 | `message`   | `X-Message`, `m`           | Filter: Only return messages that match this exact message string               |
 | `title`     | `X-Title`, `t`             | Filter: Only return messages that match this exact title string                 |
 | `priority`  | `X-Priority`, `prio`, `p`  | Filter: Only return messages that match *any priority listed* (comma-separated) |
diff --git a/server/types.go b/server/types.go
index 8c4f125bba3874f9bb73d22694e84cb9028ba77f..3f6fcdbd3f96419020b0ab99d231b7efa8bfeb53 100644
--- a/server/types.go
+++ b/server/types.go
@@ -153,6 +153,7 @@ var (
 )
 
 type queryFilter struct {
+	ID       string
 	Message  string
 	Title    string
 	Tags     []string
@@ -160,6 +161,7 @@ type queryFilter struct {
 }
 
 func parseQueryFilters(r *http.Request) (*queryFilter, error) {
+	idFilter := readParam(r, "x-id", "id")
 	messageFilter := readParam(r, "x-message", "message", "m")
 	titleFilter := readParam(r, "x-title", "title", "t")
 	tagsFilter := util.SplitNoEmpty(readParam(r, "x-tags", "tags", "tag", "ta"), ",")
@@ -172,6 +174,7 @@ func parseQueryFilters(r *http.Request) (*queryFilter, error) {
 		priorityFilter = append(priorityFilter, priority)
 	}
 	return &queryFilter{
+		ID:       idFilter,
 		Message:  messageFilter,
 		Title:    titleFilter,
 		Tags:     tagsFilter,
@@ -182,11 +185,11 @@ func parseQueryFilters(r *http.Request) (*queryFilter, error) {
 func (q *queryFilter) Pass(msg *message) bool {
 	if msg.Event != messageEvent {
 		return true // filters only apply to messages
-	}
-	if q.Message != "" && msg.Message != q.Message {
+	} else if q.ID != "" && msg.ID != q.ID {
 		return false
-	}
-	if q.Title != "" && msg.Title != q.Title {
+	} else if q.Message != "" && msg.Message != q.Message {
+		return false
+	} else if q.Title != "" && msg.Title != q.Title {
 		return false
 	}
 	messagePriority := msg.Priority