[GH-ISSUE #303] Update/delete notifications #237

Closed
opened 2026-05-07 00:21:53 +02:00 by BreizhHardware · 85 comments

Originally created by @blacklight on GitHub (Jun 1, 2022).
Original GitHub issue: https://github.com/binwiederhier/ntfy/issues/303

A similar issue was open a while ago, but it didn't get traction for some reason.

The ability to update notifications is so far the only feature that forces me to still use AutoNotification and the Tasker environment instead of ntfy.

I have a multi-host and multi-room music setup, and whenever a new track is playing I receive an actionable notification that allows me to easily control the playback.

AutoNotification allows editing a notification given its ID (even a total overwrite/replace is fine, so we don't have to mess with the complexity of partial updates). And that's exactly the feature I need: if every music device created a new notification for each track that is played the notification bar will quickly get crammed, especially if there is no programmatic way to dismiss stale notifications. Notifications that report a progress status are another quite interesting use case that isn't possible right now.

Originally created by @blacklight on GitHub (Jun 1, 2022). Original GitHub issue: https://github.com/binwiederhier/ntfy/issues/303 A [similar issue](https://github.com/binwiederhier/ntfy/issues/43) was open a while ago, but it didn't get traction for some reason. The ability to update notifications is so far the only feature that forces me to still use AutoNotification and the Tasker environment instead of ntfy. I have a multi-host and multi-room music setup, and whenever a new track is playing I receive an actionable notification that allows me to easily control the playback. AutoNotification allows editing a notification given its ID (even a total overwrite/replace is fine, so we don't have to mess with the complexity of partial updates). And that's exactly the feature I need: if every music device created a new notification for each track that is played the notification bar will quickly get crammed, especially if there is no programmatic way to dismiss stale notifications. Notifications that report a progress status are another quite interesting use case that isn't possible right now.
BreizhHardware 2026-05-07 00:21:53 +02:00
Author
Owner

@binwiederhier commented on GitHub (Jun 1, 2022):

I absolutely love the idea of updating notifications, and I've attempted to implement it twice. It's a little tricky to implement but doable.

The reason it didn't get traction was because I hated the API I came up with, and on Discord nobody had any better ideas. I want the API to be simple and ideally the request to update a message is the same that it is to initially publish it. That's what's tricky about it.

Related:
https://github.com/binwiederhier/ntfy/pull/259
#254
#187
#43

<!-- gh-comment-id:1144252757 --> @binwiederhier commented on GitHub (Jun 1, 2022): I absolutely love the idea of updating notifications, and I've attempted to implement it twice. It's a little tricky to implement but doable. The reason it didn't get traction was because I hated the API I came up with, and on Discord nobody had any better ideas. I want the API to be simple and ideally the request to update a message is the same that it is to initially publish it. That's what's tricky about it. Related: https://github.com/binwiederhier/ntfy/pull/259 #254 #187 #43
Author
Owner

@binwiederhier commented on GitHub (Jun 1, 2022):

There's also #44 for progress. That is trivial to implement once messages can be updated.

<!-- gh-comment-id:1144253055 --> @binwiederhier commented on GitHub (Jun 1, 2022): There's also #44 for progress. That is trivial to implement once messages can be updated.
Author
Owner

@blacklight commented on GitHub (Jun 2, 2022):

I want the API to be simple and ideally the request to update a message is the same that it is to initially publish it.

Something like POST/PUT without an id to create a notification and PUT with an id to upsert? id could then be passed either on the query string or the JSON payload.

<!-- gh-comment-id:1144286256 --> @blacklight commented on GitHub (Jun 2, 2022): > I want the API to be simple and ideally the request to update a message is the same that it is to initially publish it. Something like `POST`/`PUT` without an `id` to create a notification and `PUT` with an `id` to upsert? `id` could then be passed either on the query string or the JSON payload.
Author
Owner

@KawaiiZapic commented on GitHub (Jun 25, 2022):

It's great to have this feature.
we can set an id(or type) for a message, and replace the old message when new message come if id is same.

Or can we support conversation type message on Android?
conversations

It is useful to push IRC message to phone in a website only IRC channel.

<!-- gh-comment-id:1166189336 --> @KawaiiZapic commented on GitHub (Jun 25, 2022): It's great to have this feature. we can set an id(or type) for a message, and replace the old message when new message come if id is same. Or can we support [conversation type message on Android](https://developer.android.com/guide/topics/ui/conversations)? ![conversations](https://user-images.githubusercontent.com/45936772/175758022-518a2b1b-eb2c-4b3a-a1ac-ca9046f5b5be.png) It is useful to push IRC message to phone in a website only IRC channel.
Author
Owner

@mrherman commented on GitHub (Sep 13, 2022):

For the API, we could consider something similar to Home Assistants companion app's notification replace/clear system. https://companion.home-assistant.io/docs/notifications/notifications-basic/#replacing For that, the user/client provides a tag/id name for the notifications. On the receiver side, if no notification with that tag exists then it creates it, otherwise it deletes/replaces it. It is up to the user to make the tag super unique or generic depending on their use case.

Just to add, I would really like this feature too. I pop up an image of the outdoor cam when doorbell is rang, but it is only relevant until no motion is detected (for me to decide to answer it or not), after that I would like to clear the notification. Also, sticky notification (if we get them) that has some data that is periodically updated, I can update the notification when the data is changed.

<!-- gh-comment-id:1245681635 --> @mrherman commented on GitHub (Sep 13, 2022): For the API, we could consider something similar to Home Assistants companion app's notification replace/clear system. https://companion.home-assistant.io/docs/notifications/notifications-basic/#replacing For that, the user/client provides a tag/id name for the notifications. On the receiver side, if no notification with that tag exists then it creates it, otherwise it deletes/replaces it. It is up to the user to make the tag super unique or generic depending on their use case. Just to add, I would really like this feature too. I pop up an image of the outdoor cam when doorbell is rang, but it is only relevant until no motion is detected (for me to decide to answer it or not), after that I would like to clear the notification. Also, sticky notification (if we get them) that has some data that is periodically updated, I can update the notification when the data is changed.
Author
Owner

@julianlam commented on GitHub (Jan 25, 2023):

Another +1 for this suggestion, if I may 😄

We would use this in NodeBB for replacing notifications with updated text or to remove stale notifications. For example, a notification that a post needs review is no longer relevant if another admin has approved/denied it, and so the notification automatically gets rescinded in NodeBB.

We also replace notification text saying "X has posted a reply" to some variants like "X and 2 others have posted a reply", etc.

We use nid, similar to @BlackLight's suggestion. We also use mergeId to combine notifications, but I wouldn't expect that one to land in ntfy any time soon, if ever 😉

<!-- gh-comment-id:1402919495 --> @julianlam commented on GitHub (Jan 25, 2023): Another +1 for this suggestion, if I may :smile: We would use this in NodeBB for replacing notifications with updated text or to remove stale notifications. For example, a notification that a post needs review is no longer relevant if another admin has approved/denied it, and so the notification automatically gets rescinded in NodeBB. We also replace notification text saying "_X_ has posted a reply" to some variants like "_X_ and 2 others have posted a reply", etc. We use `nid`, similar to @BlackLight's suggestion. We also use `mergeId` to combine notifications, but I wouldn't expect that one to land in ntfy any time soon, if ever :wink:
Author
Owner

@courtarro commented on GitHub (Feb 24, 2023):

+1

As discussed in previous comments, it seems like the standard in other notification architectures is to use an optional ID when creating a notification. If the ID of a new notification matches a previous notification, the previous notification is replaced with the new one. If we support an integer ID, the clients should be able to handle them also. If there is an errant client that can't handle it, it would just show all of the notifications without replacement.

  • Android - takes an int natively, so we can just pass it onto the OS
  • iOS - this is a string, but an int converted to a string should be fine
  • Web - this uses the "tag" field, but you could put the ID there

Is there a reason we can't use an integer id field for this? What are the API concerns you have, @binwiederhier?

Edit: I see that there's an internal id field already that is a random string. So then let's call ours external_id and it's an int.

<!-- gh-comment-id:1442604714 --> @courtarro commented on GitHub (Feb 24, 2023): +1 As discussed in previous comments, it seems like the standard in other notification architectures is to use an optional ID when creating a notification. If the ID of a new notification matches a previous notification, the previous notification is replaced with the new one. If we support an integer ID, the clients should be able to handle them also. If there is an errant client that can't handle it, it would just show all of the notifications without replacement. - [Android](https://developer.android.com/reference/android/app/NotificationManager#notify(int,%20android.app.Notification)) - takes an int natively, so we can just pass it onto the OS - [iOS](https://developer.apple.com/documentation/usernotifications/unnotificationrequest/1649634-identifier) - this is a string, but an int converted to a string should be fine - [Web](https://developer.mozilla.org/en-US/docs/Web/API/Notifications_API/Using_the_Notifications_API#replacing_existing_notifications) - this uses the "tag" field, but you could put the ID there Is there a reason we can't use an integer `id` field for this? What are the API concerns you have, @binwiederhier? Edit: I see that there's an internal `id` field already that is a random string. So then let's call ours `external_id` and it's an int.
Author
Owner

@spraot commented on GitHub (Mar 30, 2023):

If this feature is implemented I would happily switch over from Pushover.

<!-- gh-comment-id:1490276341 --> @spraot commented on GitHub (Mar 30, 2023): If this feature is implemented I would happily switch over from Pushover.
Author
Owner

@chovyy commented on GitHub (Sep 22, 2023):

I would realy love to have this. I often send status updates from my home automation, and it is quite annoying to have this stack of messages on the same issue.

<!-- gh-comment-id:1731030126 --> @chovyy commented on GitHub (Sep 22, 2023): I would realy love to have this. I often send status updates from my home automation, and it is quite annoying to have this stack of messages on the same issue.
Author
Owner

@pnxs commented on GitHub (Nov 9, 2023):

I also really like this feature, I tried to understand what you hate about your API for updating messages but I had trouble to find what the API really looks like (I looked into #259 and #187).
You said, you want the API "to be simple and ideally the request to update a message is the same that it is to initially publish it.". This makes sense to me, because removing the message is more like adding the information, that the message is remove to an existing message. In some cases one would like to add an information or timestamp why this message was deleted.
Naively spoken: I just expect the message id to be returned from the POST, which enabled the publisher to update it's message if it want to.
To update the message it just do the same as publishing it originally, with the difference, that the message id is added to the POST.

E.g.:

First POST:

POST / HTTP/1.1
Host: ntfy.sh

{
    "topic": "myhome",
    "message": "Garage door has been open for 15 minutes. Close it?",
    "actions": [
      {
        "action": "http",
        "label": "Close door",
        "url": "https://api.mygarage.lan/",
        "method": "PUT",
        "headers": {
          "Authorization": "Bearer zAzsx1sk.."
        },
        "body": "{\"action\": \"close\"}"
      }
    ]
}

The Post will return the message Id "cdssFSfwt4fw"

Update of the message:

POST / HTTP/1.1
Host: ntfy.sh

{
    "topic": "myhome",
    "message": "Garage door has been open for 20 minutes. Close it?",
    "actions": [
      {
        "action": "http",
        "label": "Close door",
        "url": "https://api.mygarage.lan/",
        "method": "PUT",
        "headers": {
          "Authorization": "Bearer zAzsx1sk.."
        },
        "body": "{\"action\": \"close\"}"
      }
    ]
}

Remove the message:

POST / HTTP/1.1
Host: ntfy.sh

{
    "topic": "myhome",
    "id": "cdssFSfwt4fw",
    "message": "Garage door was open for 25 minutes.",
    "removed": "<timestamp>"
}

The presenting application has not to be aware of updating messages. In the simplest case, every message is shown consecutively. But if the presenting application is aware it could replace the message on it's display, change colors or do whatever. Maybe also show a log of recently remove messages.

What do you think @binwiederhier ?

<!-- gh-comment-id:1803369341 --> @pnxs commented on GitHub (Nov 9, 2023): I also really like this feature, I tried to understand what you hate about your API for updating messages but I had trouble to find what the API really looks like (I looked into #259 and #187). You said, you want the API "to be simple and ideally the request to update a message is the same that it is to initially publish it.". This makes sense to me, because removing the message is more like adding the information, that the message is remove to an existing message. In some cases one would like to add an information or timestamp why this message was deleted. Naively spoken: I just expect the message id to be returned from the POST, which enabled the publisher to update it's message if it want to. To update the message it just do the same as publishing it originally, with the difference, that the message id is added to the POST. E.g.: First POST: ``` POST / HTTP/1.1 Host: ntfy.sh { "topic": "myhome", "message": "Garage door has been open for 15 minutes. Close it?", "actions": [ { "action": "http", "label": "Close door", "url": "https://api.mygarage.lan/", "method": "PUT", "headers": { "Authorization": "Bearer zAzsx1sk.." }, "body": "{\"action\": \"close\"}" } ] } ``` The Post will return the message Id "cdssFSfwt4fw" Update of the message: ``` POST / HTTP/1.1 Host: ntfy.sh { "topic": "myhome", "message": "Garage door has been open for 20 minutes. Close it?", "actions": [ { "action": "http", "label": "Close door", "url": "https://api.mygarage.lan/", "method": "PUT", "headers": { "Authorization": "Bearer zAzsx1sk.." }, "body": "{\"action\": \"close\"}" } ] } ``` Remove the message: ``` POST / HTTP/1.1 Host: ntfy.sh { "topic": "myhome", "id": "cdssFSfwt4fw", "message": "Garage door was open for 25 minutes.", "removed": "<timestamp>" } ``` The presenting application has not to be aware of updating messages. In the simplest case, every message is shown consecutively. But if the presenting application is aware it could replace the message on it's display, change colors or do whatever. Maybe also show a log of recently remove messages. What do you think @binwiederhier ?
Author
Owner

@genofire commented on GitHub (Nov 17, 2023):

i am for POST for create, PUT for update and DELETE for delete an message as http method (and not everything POST)

i also believe everything should be equal like before the POST (just the path contains the id and topic like DELETE /topic/id), i think it is an bad idea to put any logic into the BODY of an request.

<!-- gh-comment-id:1816859459 --> @genofire commented on GitHub (Nov 17, 2023): i am for POST for create, PUT for update and DELETE for delete an message as http method (and not everything POST) i also believe everything should be equal like before the POST (just the path contains the id and topic like `DELETE /topic/id`), i think it is an bad idea to put any logic into the BODY of an request.
Author
Owner

@pnxs commented on GitHub (Nov 20, 2023):

@genofire
How to you handle the information about the time when the reason for generating the original message was omitted (e.g. the real deletion date)?
What do you mean with " i think it is an bad idea to put any logic into the BODY of an request"? I do not see any logic...
And sure you can use different Verbs, but I tried to make the API very simple, as wished by the author...

<!-- gh-comment-id:1818392954 --> @pnxs commented on GitHub (Nov 20, 2023): @genofire How to you handle the information about the time when the reason for generating the original message was omitted (e.g. the real deletion date)? What do you mean with " i think it is an bad idea to put any logic into the BODY of an request"? I do not see any logic... And sure you can use different Verbs, but I tried to make the API very simple, as wished by the author...
Author
Owner

@mikebakke commented on GitHub (Nov 20, 2023):

+1 - driven by the use case where I have set a scheduled message but need to delete it before reaching the scheduled delivery time, to cancel it as not required or correct my speeiling mistakes...

<!-- gh-comment-id:1818915351 --> @mikebakke commented on GitHub (Nov 20, 2023): +1 - driven by the use case where I have set a scheduled message but need to delete it before reaching the scheduled delivery time, to cancel it as not required or correct my speeiling mistakes...
Author
Owner

@julianlam commented on GitHub (Nov 20, 2023):

@pnxs, @genofire means you should not just have a single POST endpoint and pass in an action parameter to denote whether to create, delete, or update... because those are already available if you use the proper http methods.

<!-- gh-comment-id:1819102514 --> @julianlam commented on GitHub (Nov 20, 2023): @pnxs, @genofire means you should not just have a single POST endpoint and pass in an `action` parameter to denote whether to create, delete, or update... because those are already available if you use the proper http methods.
Author
Owner

@smashah commented on GitHub (Mar 31, 2024):

i thought this + progress bar notification would already be built in to ntfy. I think it should be implemented even if the api is awkward

<!-- gh-comment-id:2028532627 --> @smashah commented on GitHub (Mar 31, 2024): i thought this + progress bar notification would already be built in to ntfy. I think it should be implemented even if the api is awkward
Author
Owner

@wunter8 commented on GitHub (Jun 28, 2024):

Copying my thoughts from Discord to here so they don't get lost:

(a) I propose adding a new field to the message object called pid (persistent id). It will be a user-defined string ([A-Za-z0-9-_]+). You can include the pid in a header (-H "pid: garage") or in the JSON body ({"topic":"home","message":"The garage has been open for 10 minutes","pid":"garage"}).

(b) Each revision to a notification (e.g., sending another message with "pid: garage") will be its own, individual message. It will have a different message id, db row id, timestamp, etc.. I think this will help maintain backwards compatibility since old clients can just receive each individual message, ignore the pid field, and show a notification for each revision. All the messages/revisions can be sorted based on timestamp, and ?since= will work just as before. Updated clients can choose to combine/update notifications that have the same pid in their respective UIs.

(c) Having each revision be its own message (with its own message id, etc.) makes it easy for a client to show the history of a message over time. And you don't run into issues with reinserting a modified message into the db to get a new id.

(d) I prefer this approach over updating a message based on its message id (e.g., POST /topic/$id) because this way you do not need to remember/keep track of a randomly generated message id; it would be nice to update a message using a key I define.

(e) One of the use cases for this feature that was discussed in a Github issue was implementing a "progress" feature:

for progress in 10 20 30 40 50 60 70 80 90 100; do 
  curl -d "Build process running, currently ${progress}%" ntfy.sh/mytopic
  sleep 10
done

(e) Imagine this being replaced with actual progress updates of any long running command (e.g., file downloads, machine updates, machine learning model training, etc.). I could send progress updates right now using the current API (using the above example code), but I'd receive 10 different notifications. It'd be nice to be able to simply include a pid to the above code and receive 1 updating notification instead of 10 individual notifications. (And old clients that haven't been updated yet (e.g., iOS 😬) would simply "fallback" to receiving 10 individual notifications).

(f) Since every message update is its own message, rate limiting would be applied as if we were using the current/"old" system: every message or revision counts as 1 publish toward your limit; you cannot circumvent the limits by sending 1 message to a topic and "updating" the contents of that message 10 (20, 30, 100, etc.) times.

g) You mentioned that you weren't sure about anonymous users being able to update the text of any previous message. I think we can either add a new user permission to control "update" permissions (not my preference), or we can let any user that can publish to a topic update a notification with a "pid" in that topic. If we go with my approach of having each revision as its own message (and leaving it to the client to combine/update the messages in the UI), the history of edits will be tracked, so no information could be lost/overwritten. And it continues to "fallback" gracefully, since old clients will just depend on normal publish permissions instead of trying to act based on a new "update" permission. And any old, already cached notifications cannot be updated using the new feature since the old messages don't have a pid

(h) In general, this is mostly a proposal to update the clients. A new field will be added to the message object on the server, but that's it there. We don't need to add new endpoints (e.g., /topic/$id), rate-limiting code, permissions, etc.. We just add a user-specified identifier (pid) to messages (on the backend), and then update each client to deal with the new field and use it to make their respective UI/notification systems a little cleaner/more convenient.

(i) I know Android, iOS, and web allow updating the contents of a notification/replacing an existing notification with a new notification with different contents.

<!-- gh-comment-id:2197508899 --> @wunter8 commented on GitHub (Jun 28, 2024): Copying my thoughts from Discord to here so they don't get lost: (a) I propose adding a new field to the message object called `pid` (persistent id). It will be a user-defined string ([A-Za-z0-9-\_]+). You can include the `pid` in a header (`-H "pid: garage"`) or in the JSON body (`{"topic":"home","message":"The garage has been open for 10 minutes","pid":"garage"}`). (b) Each revision to a notification (e.g., sending another message with "pid: garage") will be its own, individual message. It will have a different message id, db row id, timestamp, etc.. I think this will help maintain backwards compatibility since old clients can just receive each individual message, ignore the `pid` field, and show a notification for each revision. All the messages/revisions can be sorted based on timestamp, and `?since=` will work just as before. Updated clients can choose to combine/update notifications that have the same `pid` in their respective UIs. (c) Having each revision be its own message (with its own message id, etc.) makes it easy for a client to show the history of a message over time. And you don't run into issues with reinserting a modified message into the db to get a new id. (d) I prefer this approach over updating a message based on its message id (e.g., `POST /topic/$id`) because this way you do not need to remember/keep track of a randomly generated message id; it would be nice to update a message using a key I define. (e) One of the use cases for this feature that was discussed in a Github issue was implementing a "progress" feature: ``` for progress in 10 20 30 40 50 60 70 80 90 100; do curl -d "Build process running, currently ${progress}%" ntfy.sh/mytopic sleep 10 done ``` (e) Imagine this being replaced with actual progress updates of any long running command (e.g., file downloads, machine updates, machine learning model training, etc.). I could send progress updates right now using the current API (using the above example code), but I'd receive 10 different notifications. It'd be nice to be able to simply include a `pid` to the above code and receive 1 updating notification instead of 10 individual notifications. (And old clients that haven't been updated yet (e.g., iOS 😬) would simply "fallback" to receiving 10 individual notifications). (f) Since every message update is its own message, rate limiting would be applied as if we were using the current/"old" system: every message or revision counts as 1 publish toward your limit; you cannot circumvent the limits by sending 1 message to a topic and "updating" the contents of that message 10 (20, 30, 100, etc.) times. g) You mentioned that you weren't sure about anonymous users being able to update the text of any previous message. I think we can either add a new user permission to control "update" permissions (not my preference), or we can let any user that can publish to a topic update a notification with a "pid" in that topic. If we go with my approach of having each revision as its own message (and leaving it to the client to combine/update the messages in the UI), the history of edits will be tracked, so no information could be lost/overwritten. And it continues to "fallback" gracefully, since old clients will just depend on normal publish permissions instead of trying to act based on a new "update" permission. And any old, already cached notifications cannot be updated using the new feature since the old messages don't have a `pid` (h) In general, this is mostly a proposal to update the clients. A new field will be added to the message object on the server, but that's it there. We don't need to add new endpoints (e.g., /topic/$id), rate-limiting code, permissions, etc.. We just add a user-specified identifier (`pid`) to messages (on the backend), and then update each client to deal with the new field and use it to make their respective UI/notification systems a little cleaner/more convenient. (i) I know Android, iOS, and web allow updating the contents of a notification/replacing an existing notification with a new notification with different contents.
Author
Owner

@goulf-3m commented on GitHub (Nov 24, 2024):

Please add this feature!

<!-- gh-comment-id:2495964134 --> @goulf-3m commented on GitHub (Nov 24, 2024): Please add this feature!
Author
Owner

@ElectricTea commented on GitHub (Apr 2, 2025):

Would love to see the unpublishing scheduled messages PR https://github.com/binwiederhier/ntfy/pull/1142 merged if possible ❤

<!-- gh-comment-id:2772223511 --> @ElectricTea commented on GitHub (Apr 2, 2025): Would love to see the unpublishing scheduled messages PR https://github.com/binwiederhier/ntfy/pull/1142 merged if possible ❤
Author
Owner

@binwiederhier commented on GitHub (May 26, 2025):

@wunter8 Your approach is sound and is very similar to what I would have suggested. We will likely need a version or revision field to keep track of which is the newest message in the group.

  1. I don't like the word pid. We should another name message_group_id or revision_id (rid).
  2. Can you outline (a) some mock HTTP requests and (b) how the DB would change? That would help tremendously to understand how the impl would have to change.
<!-- gh-comment-id:2910168992 --> @binwiederhier commented on GitHub (May 26, 2025): @wunter8 Your approach is sound and is very similar to what I would have suggested. We will likely need a `version` or `revision` field to keep track of which is the newest message in the group. 1. I don't like the word `pid`. We should another name `message_group_id` or `revision_id` (`rid`). 2. Can you outline (a) some mock HTTP requests and (b) how the DB would change? That would help tremendously to understand how the impl would have to change.
Author
Owner

@wunter8 commented on GitHub (May 26, 2025):

  1. The version or revision field would just be an auto-incrementing number on the backend, right? So the server would increment it when a new message is received and then send the incremented field in the server response, but it'd never be provided or incremented by the user?
  2. I think revision_id could be confused with the revision field. Are you okay with message_group_id and abbreviating it to mgid? If we use message_group_id, I think using revision would be fine.
  3. I'll work on outlining those
<!-- gh-comment-id:2910211056 --> @wunter8 commented on GitHub (May 26, 2025): 1. The `version` or `revision` field would just be an auto-incrementing number on the backend, right? So the server would increment it when a new message is received and then send the incremented field in the server response, but it'd never be provided or incremented by the user? 2. I think `revision_id` could be confused with the `revision` field. Are you okay with `message_group_id` and abbreviating it to `mgid`? If we use `message_group_id`, I think using `revision` would be fine. 3. I'll work on outlining those
Author
Owner

@mrherman commented on GitHub (May 26, 2025):

THe message group ID would be user defined right, what are the constraints,
any ASCII or alphanumeric field? Length? What happens if it is not set?
Just blank/null and that's treated as no mgid? I think that would give the
backwards compatibility desired.

This feature also requested delete notifications. I think deep down that
means remove them from the users current notifications, or atleast unread
notifications. I don't think it needs to mean delete from the database,
especially as the discussion above is just a series of updates/rows in the
database. I know I would want to remove a notification from my phone's
pull down notifications. Would there be a way to do that? I might propose
just an update with a blank/empty message? A client would need to know
that means don't create a new message, but remove the current. BUT, I
imagine some users might use blank messages with just a topic already, if
so this would be bad for them. An alternative would be an additional field
that could be set: dont_show, or remove or something similar? The client
would know to update the current mgid group by removing the notice of the
last message.

The use case for being able to remove a notification:
My system alerts me if the garage door is open for over 10 minutes, a
reminder so I could close it if needed.. Sometimes, it's just my kids and
they close it after 20 min. When the garage door finally closes I don't
need to pop up a notification on my phone anymore, the issue is resolved.
I pop up a ntfy when my door bell rings with a picture of who it is. I can
decide to answer the door with that info. But after 5 min it doesn't
matter if I saw it or not, they already left and I can remove the ntfy. (I
don't want a ntfy on my phone of all the people that have been at my door,
just who is there right now).

On Mon, May 26, 2025 at 12:20 PM wunter8 @.***> wrote:

wunter8 left a comment (binwiederhier/ntfy#303)
https://github.com/binwiederhier/ntfy/issues/303#issuecomment-2910211056

  1. The version or revision field would just be an auto-incrementing
    number on the backend, right? So the server would increment it when a new
    message is received and then send the incremented field in the server
    response, but it'd never be provided or incremented by the user?
  2. I think revision_id could be confused with the revision field. Are
    you okay with message_group_id and abbreviating it to mgid? If we use
    message_group_id, I think using revision would be fine.
  3. I'll work on outlining those


Reply to this email directly, view it on GitHub
https://github.com/binwiederhier/ntfy/issues/303#issuecomment-2910211056,
or unsubscribe
https://github.com/notifications/unsubscribe-auth/AANRCZYTXMITVBAY74B5GVD3AM5M5AVCNFSM5XS7KYZKU5DIOJSWCZC7NNSXTN2JONZXKZKDN5WW2ZLOOQ5TEOJRGAZDCMJQGU3A
.
You are receiving this because you commented.Message ID:
@.***>

<!-- gh-comment-id:2910243711 --> @mrherman commented on GitHub (May 26, 2025): THe message group ID would be user defined right, what are the constraints, any ASCII or alphanumeric field? Length? What happens if it is not set? Just blank/null and that's treated as no mgid? I think that would give the backwards compatibility desired. This feature also requested delete notifications. I think deep down that means remove them from the users current notifications, or atleast unread notifications. I don't think it needs to mean delete from the database, especially as the discussion above is just a series of updates/rows in the database. I know I would want to remove a notification from my phone's pull down notifications. Would there be a way to do that? I might propose just an update with a blank/empty message? A client would need to know that means don't create a new message, but remove the current. BUT, I imagine some users might use blank messages with just a topic already, if so this would be bad for them. An alternative would be an additional field that could be set: dont_show, or remove or something similar? The client would know to update the current mgid group by removing the notice of the last message. The use case for being able to remove a notification: My system alerts me if the garage door is open for over 10 minutes, a reminder so I could close it if needed.. Sometimes, it's just my kids and they close it after 20 min. When the garage door finally closes I don't need to pop up a notification on my phone anymore, the issue is resolved. I pop up a ntfy when my door bell rings with a picture of who it is. I can decide to answer the door with that info. But after 5 min it doesn't matter if I saw it or not, they already left and I can remove the ntfy. (I don't want a ntfy on my phone of all the people that have been at my door, just who is there right now). On Mon, May 26, 2025 at 12:20 PM wunter8 ***@***.***> wrote: > *wunter8* left a comment (binwiederhier/ntfy#303) > <https://github.com/binwiederhier/ntfy/issues/303#issuecomment-2910211056> > > 1. The version or revision field would just be an auto-incrementing > number on the backend, right? So the server would increment it when a new > message is received and then send the incremented field in the server > response, but it'd never be provided or incremented by the user? > 2. I think revision_id could be confused with the revision field. Are > you okay with message_group_id and abbreviating it to mgid? If we use > message_group_id, I think using revision would be fine. > 3. I'll work on outlining those > > — > Reply to this email directly, view it on GitHub > <https://github.com/binwiederhier/ntfy/issues/303#issuecomment-2910211056>, > or unsubscribe > <https://github.com/notifications/unsubscribe-auth/AANRCZYTXMITVBAY74B5GVD3AM5M5AVCNFSM5XS7KYZKU5DIOJSWCZC7NNSXTN2JONZXKZKDN5WW2ZLOOQ5TEOJRGAZDCMJQGU3A> > . > You are receiving this because you commented.Message ID: > ***@***.***> >
Author
Owner

@binwiederhier commented on GitHub (May 26, 2025):

Let's see what the proposal looks like. It's much easier to discuss when we have an actual proposal.

Use cases we want to support:

  • Logically updating an existing message
  • Logically deleting an existing message
  • Backwards compatibilityy
<!-- gh-comment-id:2910318386 --> @binwiederhier commented on GitHub (May 26, 2025): Let's see what the proposal looks like. It's much easier to discuss when we have an actual proposal. Use cases we want to support: - Logically updating an existing message - Logically deleting an existing message - Backwards compatibilityy
Author
Owner

@wunter8 commented on GitHub (May 26, 2025):

For "logically deleting an existing message", are you hoping to support deleting messages that don't have a message_group_id (e.g., deleting a message based on its id alone instead of based on its mgid)?

<!-- gh-comment-id:2910329931 --> @wunter8 commented on GitHub (May 26, 2025): For "logically deleting an existing message", are you hoping to support deleting messages that don't have a message_group_id (e.g., deleting a message based on its `id` alone instead of based on its `mgid`)?
Author
Owner

@binwiederhier commented on GitHub (May 26, 2025):

I am hoping for all the things :-D We could just assign an group id (uhhh, update_id?) to every message, that can then be used to update it. Because it would be a bummer if you'd have to remember to pass the group id to update it later.

<!-- gh-comment-id:2910334285 --> @binwiederhier commented on GitHub (May 26, 2025): I am hoping for all the things :-D We could just assign an group id (uhhh, `update_id`?) to every message, that can then be used to update it. Because it would be a bummer if you'd have to remember to pass the group id to update it later.
Author
Owner

@wunter8 commented on GitHub (May 26, 2025):

So allow a user to specify an update_id (renaming from message_group_id), but if no update_id (uid) is provided, then we randomly generate one and send it to the user so they can delete the message later if they store that randomly generated uid?

<!-- gh-comment-id:2910342076 --> @wunter8 commented on GitHub (May 26, 2025): So allow a user to specify an `update_id` (renaming from `message_group_id`), but if no `update_id` (`uid`) is provided, then we randomly generate one and send it to the user so they can delete the message later if they store that randomly generated `uid`?
Author
Owner

@binwiederhier commented on GitHub (May 26, 2025):

Something like that?! Or we just implicitly use the message id as the update id if you don't specify one. Idk :)

<!-- gh-comment-id:2910343799 --> @binwiederhier commented on GitHub (May 26, 2025): Something like that?! Or we just implicitly use the message id as the update id if you don't specify one. Idk :)
Author
Owner

@mrherman commented on GitHub (May 26, 2025):

I don't think it is unreasonable to expect the user to be able to set and
remember a message group ID if they want to update and "logically remove" a
message group. If not set, something random if it's needed for the
database to work, or just null (and you can't send updates/remove).

On Mon, May 26, 2025 at 1:42 PM wunter8 @.***> wrote:

wunter8 left a comment (binwiederhier/ntfy#303)
https://github.com/binwiederhier/ntfy/issues/303#issuecomment-2910342076

So allow a user to specify an update_id (renaming from message_group_id),
but if no update_id (uid) is provided, then we randomly generate one and
send it to the user so they can delete the message later if they store that
randomly generated uid?


Reply to this email directly, view it on GitHub
https://github.com/binwiederhier/ntfy/issues/303#issuecomment-2910342076,
or unsubscribe
https://github.com/notifications/unsubscribe-auth/AANRCZ6HJKPKQBMYOQ4ALTD3ANHCFAVCNFSM5XS7KYZKU5DIOJSWCZC7NNSXTN2JONZXKZKDN5WW2ZLOOQ5TEOJRGAZTIMRQG43A
.
You are receiving this because you commented.Message ID:
@.***>

<!-- gh-comment-id:2910348054 --> @mrherman commented on GitHub (May 26, 2025): I don't think it is unreasonable to expect the user to be able to set and remember a message group ID if they want to update and "logically remove" a message group. If not set, something random if it's needed for the database to work, or just null (and you can't send updates/remove). On Mon, May 26, 2025 at 1:42 PM wunter8 ***@***.***> wrote: > *wunter8* left a comment (binwiederhier/ntfy#303) > <https://github.com/binwiederhier/ntfy/issues/303#issuecomment-2910342076> > > So allow a user to specify an update_id (renaming from message_group_id), > but if no update_id (uid) is provided, then we randomly generate one and > send it to the user so they can delete the message later if they store that > randomly generated uid? > > — > Reply to this email directly, view it on GitHub > <https://github.com/binwiederhier/ntfy/issues/303#issuecomment-2910342076>, > or unsubscribe > <https://github.com/notifications/unsubscribe-auth/AANRCZ6HJKPKQBMYOQ4ALTD3ANHCFAVCNFSM5XS7KYZKU5DIOJSWCZC7NNSXTN2JONZXKZKDN5WW2ZLOOQ5TEOJRGAZTIMRQG43A> > . > You are receiving this because you commented.Message ID: > ***@***.***> >
Author
Owner

@p1gp1g commented on GitHub (May 28, 2025):

Web push has this exact feature: https://www.rfc-editor.org/rfc/rfc8030#section-5.4
It uses the Topic header. If you implement the feature, it would be great to use this header when ?up=1, to bring it to UnifiedPush as well

<!-- gh-comment-id:2915321072 --> @p1gp1g commented on GitHub (May 28, 2025): Web push has this exact feature: https://www.rfc-editor.org/rfc/rfc8030#section-5.4 It uses the `Topic` header. If you implement the feature, it would be great to use this header when `?up=1`, to bring it to UnifiedPush as well
Author
Owner

@wunter8 commented on GitHub (May 29, 2025):

Here's a wall of text carefully prepared proposal!

DB Changes

ALTER TABLE messages ADD COLUMN uid TEXT NOT NULL DEFAULT('');
ALTER TABLE messages ADD COLUMN revision INTEGER NOT NULL DEFAULT('1');

getMostRecentRevisionByUid: SELECT revision FROM messages WHERE uid = ? ORDER BY revision DESC LIMIT 1; (used when inserting new message/revision into DB so we can add 1 to the revision)

HTTP Request Examples

Initial message without a uid. uid made equal to randomly generated id

POST /mytopic HTTP/1.1
Host: ntfy.sh

message
{"id":"abc123", "uid":"abc123", "time":1748501200, "expires":1748503200, "event":"message", "topic":"mytopic", "message":"message", "revision":1}

Update a message using uid

POST /mytopic HTTP/1.1
Host: ntfy.sh
Uid: abc123

second message
{"id":"def456", "uid":"abc123", "time":1748501300, "expires":1748503300, "event":"message", "topic":"mytopic", "message":"second message", "revision":2}

Deleting a message using uid (revision is incremented, message body is cleared/set to empty string). The ntfy server does not let a user send an empty message body right now (if a user tries to do that, ntfy replaces the message with "triggered"), so I think it's safe to use an empty message body as an indication that the notification should be cleared.

DELETE /mytopic HTTP/1.1
Host: ntfy.sh
Uid: abc123
{"id":"ghi789", "uid":"abc123", "time":1748501400, "expires":1748503400, "event":"message", "topic":"mytopic", "message":"", "revision":3}

Updating a deleted message (same as previous update)

POST /mytopic HTTP/1.1
Host: ntfy.sh
Uid: abc123

another message here
{"id":"jkl012", "uid":"abc123", "time":1748501500, "expires":1748503500, "event":"message", "topic":"mytopic", "message":"another message here", "revision":4}

A message with a custom uid (these all work with either POST or PUT)

PUT /mytopic HTTP/1.1
Host: ntfy.sh
Uid: updatable

This is the first message
{"id":"mno345", "uid":"updatable", "time":1748501600, "expires":1748503600, "event":"message", "topic":"mytopic", "message":"This is the first message", "revision":1}

A message via GET with query params

GET /mytopic?message=testing&uid=testing HTTP/1.1
Host: ntfy.sh
{"id":"pqr678", "uid":"testing", "time":1748501700, "expires":1748503700, "event":"message", "topic":"mytopic", "message":"testing", "revision":1}

Update via GET with query params

GET /mytopic?message=testing+again&uid=testing HTTP/1.1
Host: ntfy.sh
{"id":"stu901", "uid":"testing", "time":1748501800, "expires":1748503800, "event":"message", "topic":"mytopic", "message":"testing again", "revision":2}

Scheduled Messages

I think this structure could also be used to support the "deadmanssnitch" feature #254 with the existing at/delay/in headers.

Schedule a message with a delay

POST /mytopic HTTP/1.1
Host: ntfy.sh
Uid: canary
In: 10 min

Server hasn't responded for 10 minutes
{"id":"vwx234", "uid":"canary", "time":1748501900, "expires":1748503900, "event":"message", "topic":"mytopic", "message":"Server hasn't responded for 10 minutes", "revision":1}

Postpone the message (same request, new message id in response, server removes previous message id from "scheduled" queue (by marking the message as already published? or by changing its time field to now-1 so the message would have already been sent?))

POST /mytopic HTTP/1.1
Host: ntfy.sh
Uid: canary
In: 10 min

Server hasn't responded for 10 minutes
{"id":"yza567", "uid":"canary", "time":1748502000, "expires":1748504000, "event":"message", "topic":"mytopic", "message":"Server hasn't responded for 10 minutes", "revision":2}

Other Fields

Other fields (e.g., title, priority, tags, actions, attachments, etc.) can be added during any request (except for a DELETE). The new message will include the additional fields, like any other message would. If a first message includes a tag and a second message doesn't include the tag, the second message will appear on the client device without the tag from the first message.

Subscribe API

We probably need a new value for the subscribe API to only return the latest message for each uid from the cache vs. returning all messages with the same uid that are in the cache

Get only latest revisions

GET /mytopic?poll=1&revisions=latest HTTP/1.1
Host: ntfy.sh
{"id":"jkl012", "uid":"abc123", "time":1748501500, "expires":1748503500, "event":"message", "topic":"mytopic", "message":"another message here", "revision":4}
{"id":"mno345", "uid":"updatable", "time":1748501600, "expires":1748503600, "event":"message", "topic":"mytopic", "message":"This is the first message", "revision":1}
{"id":"stu901", "uid":"testing", "time":1748501800, "expires":1748503800, "event":"message", "topic":"mytopic", "message":"testing again", "revision":2}

Get all revisions (default for backwards compatibility. clients may only create a push notification for the latest message of each uid, though)

GET /mytopic?poll=1&revisions=all HTTP/1.1
Host: ntfy.sh
{"id":"abc123", "uid":"abc123", "time":1748501200, "expires":1748503200, "event":"message", "topic":"mytopic", "message":"message", "revision":1}
{"id":"def456", "uid":"abc123", "time":1748501300, "expires":1748503300, "event":"message", "topic":"mytopic", "message":"second message", "revision":2}
{"id":"ghi789", "uid":"abc123", "time":1748501400, "expires":1748503400, "event":"message", "topic":"mytopic", "message":"", "revision":3}
{"id":"jkl012", "uid":"abc123", "time":1748501500, "expires":1748503500, "event":"message", "topic":"mytopic", "message":"another message here", "revision":4}
{"id":"mno345", "uid":"updatable", "time":1748501600, "expires":1748503600, "event":"message", "topic":"mytopic", "message":"This is the first message", "revision":1}
{"id":"pqr678", "uid":"testing", "time":1748501700, "expires":1748503700, "event":"message", "topic":"mytopic", "message":"testing", "revision":1}
{"id":"stu901", "uid":"testing", "time":1748501800, "expires":1748503800, "event":"message", "topic":"mytopic", "message":"testing again", "revision":2}

Questions

  1. Do we support deleting messages via POST or PUT? I am thinking some services that support webhooks may only have GET and POST as options, so they may not be able to send a DELETE request (this is just my gut impression; I haven't looked at specific services and couldn't tell you, for example, that X service doesn't support DELETE)
  2. If we do support deleting a message via POST or PUT, what would that syntax look like? We'd need an additional header/query param in that case. Should that be the only option instead of using the DELETE HTTP method?
  3. For updating a scheduled message, would we prefer to simply update the time and expires fields instead of creating a new message with a unique id each time? This might be easier/nicer, but we would lose the history of messages (just in the server cache), if the messages happened to change over time. This might be fine
<!-- gh-comment-id:2918222846 --> @wunter8 commented on GitHub (May 29, 2025): Here's a ~~wall of text~~ carefully prepared proposal! ## DB Changes ``` ALTER TABLE messages ADD COLUMN uid TEXT NOT NULL DEFAULT(''); ALTER TABLE messages ADD COLUMN revision INTEGER NOT NULL DEFAULT('1'); ``` `getMostRecentRevisionByUid`: `SELECT revision FROM messages WHERE uid = ? ORDER BY revision DESC LIMIT 1;` (used when inserting new message/revision into DB so we can add 1 to the revision) ## HTTP Request Examples Initial message without a `uid`. `uid` made equal to randomly generated `id` ``` POST /mytopic HTTP/1.1 Host: ntfy.sh message ``` ``` {"id":"abc123", "uid":"abc123", "time":1748501200, "expires":1748503200, "event":"message", "topic":"mytopic", "message":"message", "revision":1} ``` Update a message using `uid` ``` POST /mytopic HTTP/1.1 Host: ntfy.sh Uid: abc123 second message ``` ``` {"id":"def456", "uid":"abc123", "time":1748501300, "expires":1748503300, "event":"message", "topic":"mytopic", "message":"second message", "revision":2} ``` Deleting a message using `uid` (revision is incremented, message body is cleared/set to empty string). The ntfy server does not let a user send an empty message body right now (if a user tries to do that, ntfy replaces the message with "triggered"), so I think it's safe to use an empty message body as an indication that the notification should be cleared. ``` DELETE /mytopic HTTP/1.1 Host: ntfy.sh Uid: abc123 ``` ``` {"id":"ghi789", "uid":"abc123", "time":1748501400, "expires":1748503400, "event":"message", "topic":"mytopic", "message":"", "revision":3} ``` Updating a deleted message (same as previous update) ``` POST /mytopic HTTP/1.1 Host: ntfy.sh Uid: abc123 another message here ``` ``` {"id":"jkl012", "uid":"abc123", "time":1748501500, "expires":1748503500, "event":"message", "topic":"mytopic", "message":"another message here", "revision":4} ``` A message with a custom `uid` (these all work with either POST or PUT) ``` PUT /mytopic HTTP/1.1 Host: ntfy.sh Uid: updatable This is the first message ``` ``` {"id":"mno345", "uid":"updatable", "time":1748501600, "expires":1748503600, "event":"message", "topic":"mytopic", "message":"This is the first message", "revision":1} ``` A message via GET with query params ``` GET /mytopic?message=testing&uid=testing HTTP/1.1 Host: ntfy.sh ``` ``` {"id":"pqr678", "uid":"testing", "time":1748501700, "expires":1748503700, "event":"message", "topic":"mytopic", "message":"testing", "revision":1} ``` Update via GET with query params ``` GET /mytopic?message=testing+again&uid=testing HTTP/1.1 Host: ntfy.sh ``` ``` {"id":"stu901", "uid":"testing", "time":1748501800, "expires":1748503800, "event":"message", "topic":"mytopic", "message":"testing again", "revision":2} ``` ## Scheduled Messages I think this structure could also be used to support the "deadmanssnitch" feature #254 with the existing `at`/`delay`/`in` headers. Schedule a message with a delay ``` POST /mytopic HTTP/1.1 Host: ntfy.sh Uid: canary In: 10 min Server hasn't responded for 10 minutes ``` ``` {"id":"vwx234", "uid":"canary", "time":1748501900, "expires":1748503900, "event":"message", "topic":"mytopic", "message":"Server hasn't responded for 10 minutes", "revision":1} ``` Postpone the message (same request, new message id in response, server removes previous message id from "scheduled" queue (by marking the message as already published? or by changing its `time` field to `now-1` so the message would have already been sent?)) ``` POST /mytopic HTTP/1.1 Host: ntfy.sh Uid: canary In: 10 min Server hasn't responded for 10 minutes ``` ``` {"id":"yza567", "uid":"canary", "time":1748502000, "expires":1748504000, "event":"message", "topic":"mytopic", "message":"Server hasn't responded for 10 minutes", "revision":2} ``` ## Other Fields Other fields (e.g., title, priority, tags, actions, attachments, etc.) can be added during any request (except for a DELETE). The new message will include the additional fields, like any other message would. If a first message includes a tag and a second message doesn't include the tag, the second message will appear on the client device without the tag from the first message. ## Subscribe API We probably need a new value for the subscribe API to only return the latest message for each `uid` from the cache vs. returning all messages with the same `uid` that are in the cache Get only latest revisions ``` GET /mytopic?poll=1&revisions=latest HTTP/1.1 Host: ntfy.sh ``` ``` {"id":"jkl012", "uid":"abc123", "time":1748501500, "expires":1748503500, "event":"message", "topic":"mytopic", "message":"another message here", "revision":4} {"id":"mno345", "uid":"updatable", "time":1748501600, "expires":1748503600, "event":"message", "topic":"mytopic", "message":"This is the first message", "revision":1} {"id":"stu901", "uid":"testing", "time":1748501800, "expires":1748503800, "event":"message", "topic":"mytopic", "message":"testing again", "revision":2} ``` Get all revisions (default for backwards compatibility. clients may only create a push notification for the latest message of each `uid`, though) ``` GET /mytopic?poll=1&revisions=all HTTP/1.1 Host: ntfy.sh ``` ``` {"id":"abc123", "uid":"abc123", "time":1748501200, "expires":1748503200, "event":"message", "topic":"mytopic", "message":"message", "revision":1} {"id":"def456", "uid":"abc123", "time":1748501300, "expires":1748503300, "event":"message", "topic":"mytopic", "message":"second message", "revision":2} {"id":"ghi789", "uid":"abc123", "time":1748501400, "expires":1748503400, "event":"message", "topic":"mytopic", "message":"", "revision":3} {"id":"jkl012", "uid":"abc123", "time":1748501500, "expires":1748503500, "event":"message", "topic":"mytopic", "message":"another message here", "revision":4} {"id":"mno345", "uid":"updatable", "time":1748501600, "expires":1748503600, "event":"message", "topic":"mytopic", "message":"This is the first message", "revision":1} {"id":"pqr678", "uid":"testing", "time":1748501700, "expires":1748503700, "event":"message", "topic":"mytopic", "message":"testing", "revision":1} {"id":"stu901", "uid":"testing", "time":1748501800, "expires":1748503800, "event":"message", "topic":"mytopic", "message":"testing again", "revision":2} ``` ## Questions 1. Do we support deleting messages via POST or PUT? I am thinking some services that support webhooks may only have GET and POST as options, so they may not be able to send a DELETE request (this is just my gut impression; I haven't looked at specific services and couldn't tell you, for example, that X service doesn't support DELETE) 2. If we do support deleting a message via POST or PUT, what would that syntax look like? We'd need an additional header/query param in that case. Should that be the only option instead of using the DELETE HTTP method? 3. For updating a scheduled message, would we prefer to simply update the `time` and `expires` fields instead of creating a new message with a unique id each time? This might be easier/nicer, but we would lose the history of messages (just in the server cache), if the messages happened to change over time. This might be fine
Author
Owner

@binwiederhier commented on GitHub (May 29, 2025):

(I just saw this; I will review tonight or tmr! Looks promising.)

<!-- gh-comment-id:2920672845 --> @binwiederhier commented on GitHub (May 29, 2025): (I just saw this; I will review tonight or tmr! Looks promising.)
Author
Owner

@wunter8 commented on GitHub (Jun 1, 2025):

Thinking a bit more about the scheduled messages, I think we should only keep 1 scheduled message for a particular uid at a time. So a first message can be scheduled with uid: reminder. Then, if we want to update that (push the reminder out in the future, change the reminder message, etc.), we can POST a new message with uid: reminder and whatever fields we want (message, delay, attachments, etc.). The previous revision would be removed from the DB (I'm thinking deleted entirely), and the new revision would be added/scheduled to be sent in the future.

I think deleting entirely might be better than modifying it's timestamp field or published field because if we change the DB to show the message was published and don't send anything to the client (since it wasn't actually sent), if the client polls the server for all messages in the cache for a topic, they'd get a bunch of scheduled revisions that were never supposed to be sent.

If we delete the revisions, using the subscriber API, you could still check for scheduled messages on a particular topic, but you wouldn't be able to see a history of revisions for the scheduled message, only the most recent.

To delete/unschedule a future message, you'd DELETE /topic?uid=reminder (or something like POST /topic?uid=reminder&delete if we decide to support that). The previously scheduled message would be removed from the DB and a new message with empty content would be put in its place. Or, when deleting scheduled messages, we could remove them from the DB and not replace it with a message with empty content since messages with empty content are specifically for clearing notifications on a client device. With the scheduled message, since it hasn't been sent yet, it won't be on the client device at all.

However, I'm now considering the scenario when you send (assuming all these have the same uid) message A then immediately send message B with a 10 minute delay. Then there are two messages in the cache for that uid: A which has been published and B which hasn't been published. If you then send DELETE /topic?uid=..., what should happen?

Option A: the scheduled message is deleted/unscheduled
Option B: the published message A is cleared from the notifications on client devices
Option C: both A and B
Option D: we add a parameter to allow the user to choose when they send the DELETE request??

<!-- gh-comment-id:2927423013 --> @wunter8 commented on GitHub (Jun 1, 2025): Thinking a bit more about the scheduled messages, I think we should only keep 1 scheduled message for a particular `uid` at a time. So a first message can be scheduled with `uid: reminder`. Then, if we want to update that (push the reminder out in the future, change the reminder message, etc.), we can POST a new message with `uid: reminder` and whatever fields we want (message, delay, attachments, etc.). The previous revision would be removed from the DB (I'm thinking deleted entirely), and the new revision would be added/scheduled to be sent in the future. I think deleting entirely might be better than modifying it's timestamp field or published field because if we change the DB to show the message was published and don't send anything to the client (since it wasn't actually sent), if the client polls the server for all messages in the cache for a topic, they'd get a bunch of scheduled revisions that were never supposed to be sent. If we delete the revisions, using the subscriber API, you could still check for scheduled messages on a particular topic, but you wouldn't be able to see a history of revisions for the scheduled message, only the most recent. To delete/unschedule a future message, you'd `DELETE /topic?uid=reminder` (or something like `POST /topic?uid=reminder&delete` if we decide to support that). The previously scheduled message would be removed from the DB and a new message with empty content would be put in its place. Or, when deleting scheduled messages, we could remove them from the DB and not replace it with a message with empty content since messages with empty content are specifically for clearing notifications on a client device. With the scheduled message, since it hasn't been sent yet, it won't be on the client device at all. However, I'm now considering the scenario when you send (assuming all these have the same `uid`) message `A` then immediately send message `B` with a 10 minute delay. Then there are two messages in the cache for that `uid`: `A` which has been published and `B` which hasn't been published. If you then send `DELETE /topic?uid=...`, what should happen? Option A: the scheduled message is deleted/unscheduled Option B: the published message A is cleared from the notifications on client devices Option C: both A and B Option D: we add a parameter to allow the user to choose when they send the DELETE request??
Author
Owner

@wunter8 commented on GitHub (Jun 1, 2025):

I guess a similar problem would arise if they send A then B with a delay, and then they send a POST /topic?uid=... to update the message. Does that request update the contents of message A on client devicea? The contents of (scheduled) message B in the DB? Both? Option D from above?

<!-- gh-comment-id:2927477573 --> @wunter8 commented on GitHub (Jun 1, 2025): I guess a similar problem would arise if they send `A` then `B` with a delay, and then they send a `POST /topic?uid=...` to update the message. Does that request update the contents of message `A` on client devicea? The contents of (scheduled) message `B` in the DB? Both? Option D from above?
Author
Owner

@gbansaghi commented on GitHub (Jun 1, 2025):

Questions

  1. Do we support deleting messages via POST or PUT? I am thinking some services that support webhooks may only have GET and POST as options, so they may not be able to send a DELETE request (this is just my gut impression; I haven't looked at specific services and couldn't tell you, for example, that X service doesn't support DELETE)

I think it makes the most sense to use DELETE and provide a webhook that can be used with GET (say, /delete). This would mirror the current methods for sending messages (POST or PUT, or the /publish webhook via GET) and could be extended to updating messages too. Something like:

Action HTTP method Webhook
Send POST or PUT /publish (or /send or /trigger)
Update PUT /update (or maybe the same as above?)
Delete DELETE /delete
<!-- gh-comment-id:2927574700 --> @gbansaghi commented on GitHub (Jun 1, 2025): > ## Questions > > 1. Do we support deleting messages via POST or PUT? I am thinking some services that support webhooks may only have GET and POST as options, so they may not be able to send a DELETE request (this is just my gut impression; I haven't looked at specific services and couldn't tell you, for example, that X service doesn't support DELETE) I think it makes the most sense to use DELETE and provide a webhook that can be used with GET (say, `/delete`). This would mirror the current methods for sending messages (POST or PUT, or the `/publish` webhook via GET) and could be extended to updating messages too. Something like: | Action | HTTP method | Webhook | | ---- | ---- | ---- | | Send | POST or PUT | `/publish` (or `/send` or `/trigger`) | | Update | PUT | `/update` (or maybe the same as above?) | | Delete | DELETE | `/delete` |
Author
Owner

@wunter8 commented on GitHub (Jun 1, 2025):

@gbansaghi I like that idea. I don't think we'll distinguish between "send" and "update." I think they'll both just use the /publish endpoint, but we can add a /delete endpoint that requires a uid as either a header or query param.

<!-- gh-comment-id:2927634866 --> @wunter8 commented on GitHub (Jun 1, 2025): @gbansaghi I like that idea. I don't think we'll distinguish between "send" and "update." I think they'll both just use the /publish endpoint, but we can add a /delete endpoint that requires a `uid` as either a header or query param.
Author
Owner

@wunter8 commented on GitHub (Jun 1, 2025):

In response to my questions about scheduled messages, how about this:

To update/delete a sent message, you need to include uid and CANNOT include a delay.

To update/delete a scheduled message, you need to include uid and MUST include a delay.

For example:

Send message A and B with a delay:

curl -d A -H "uid:reminder" ntfy.sh/topic && \
curl -d B -H "uid:reminder" -H "delay: 10 min" ntfy.sh/topic

Update message A on client devices:

curl -d "new A" -H "uid:reminder" ntfy.sh/topic

Update scheduled message B in DB:

curl -d "new B" -H "uid:reminder" -H "delay: 10 min" ntfy.sh/topic

The 4th curl command above would postpone the scheduled message another 10 minutes. So the message would arrive 10 minutes after the 4th curl command and not 10 minutes after the 2nd curl command.

To avoid further delaying the scheduled message (e.g., to only update it's message contents), I think we need a new delay value. We could do 0? Or any negative number? Or, since the delay param is a string that we parse into a duration, we could use a string like "keep"?

<!-- gh-comment-id:2927658296 --> @wunter8 commented on GitHub (Jun 1, 2025): In response to my questions about scheduled messages, how about this: To update/delete a sent message, you need to include `uid` and CANNOT include a delay. To update/delete a scheduled message, you need to include `uid` and MUST include a delay. For example: Send message A and B with a delay: ``` curl -d A -H "uid:reminder" ntfy.sh/topic && \ curl -d B -H "uid:reminder" -H "delay: 10 min" ntfy.sh/topic ``` Update message A on client devices: ``` curl -d "new A" -H "uid:reminder" ntfy.sh/topic ``` Update scheduled message B in DB: ``` curl -d "new B" -H "uid:reminder" -H "delay: 10 min" ntfy.sh/topic ``` The 4th curl command above would postpone the scheduled message another 10 minutes. So the message would arrive 10 minutes after the 4th curl command and not 10 minutes after the 2nd curl command. To avoid further delaying the scheduled message (e.g., to only update it's message contents), I think we need a new `delay` value. We could do 0? Or any negative number? Or, since the `delay` param is a string that we parse into a duration, we could use a string like "keep"?
Author
Owner

@gbansaghi commented on GitHub (Jun 1, 2025):

Disclaimer: I am very new to ntfy, so it's likely that there's implementation details I'm not understanding fully. That being said, my approach would be:

If:

  • an update is received for a uid
  • update includes a delay
  • latest revision for the uid does not include a delay

then assume the user meant "I changed my mind, I don't want to show this until later" and

  • clear the message (as if a DELETE was received, by sending an empty message)
  • schedule the message with the newly specified delay

Essentially, a given uid can be either currently displayed to clients, or scheduled for later, but not both.

Latest rev has delay Latest rev has no delay
Update with delay Change delay to new value Clear message, then schedule with delay
Update without delay Show immediately Update message

There's probably terminology I'm misusing, but hopefully I've made the basic idea clear. Does this seem technically feasible?

<!-- gh-comment-id:2927754695 --> @gbansaghi commented on GitHub (Jun 1, 2025): Disclaimer: I am very new to ntfy, so it's likely that there's implementation details I'm not understanding fully. That being said, my approach would be: If: - an update is received for a `uid` - update includes a delay - latest revision for the `uid` does _not_ include a delay then assume the user meant "I changed my mind, I don't want to show this until later" and - clear the message (as if a DELETE was received, by sending an empty message) - schedule the message with the newly specified delay Essentially, a given `uid` can be either currently displayed to clients, or scheduled for later, but not both. | | Latest rev has delay | Latest rev has no delay | | --- | --- | --- | | Update with delay | Change delay to new value | Clear message, then schedule with delay | | Update without delay | Show immediately | Update message | There's probably terminology I'm misusing, but hopefully I've made the basic idea clear. Does this seem technically feasible?
Author
Owner

@wunter8 commented on GitHub (Jun 1, 2025):

That would mean that you can never update a scheduled message after it's received, right? (or at least not until it has expired from the cache, which some people set to very long period (e.g., 1+ years)). I don't think we want to restrict uid to "currently displayed or scheduled but not both."

<!-- gh-comment-id:2927844266 --> @wunter8 commented on GitHub (Jun 1, 2025): That would mean that you can never update a scheduled message after it's received, right? (or at least not until it has expired from the cache, which some people set to very long period (e.g., 1+ years)). I don't think we want to restrict `uid` to "currently displayed or scheduled but not both."
Author
Owner

@gbansaghi commented on GitHub (Jun 1, 2025):

That would mean that you can never update a scheduled message after it's received, right?

If my understanding is correct (and it may well not be), updating a scheduled message without a delay would add a revision that's not scheduled, and is therefore delivered immediately. If the message was already received, the client would see the matching uid and change the existing notification. Otherwise, it would show up as a new message.

<!-- gh-comment-id:2927972048 --> @gbansaghi commented on GitHub (Jun 1, 2025): > That would mean that you can never update a scheduled message after it's received, right? If my understanding is correct (and it may well not be), updating a scheduled message without a delay would add a revision that's not scheduled, and is therefore delivered immediately. If the message was already received, the client would see the matching `uid` and change the existing notification. Otherwise, it would show up as a new message.
Author
Owner

@wunter8 commented on GitHub (Jun 1, 2025):

That part of your understanding is correct. Here's a bit more about the current ntfy architecture.

All messages are stored in the same cache table. Messages have a timestamp and a published boolean. When a no-delay message is received by the server, it puts it in the cache with timestamp=now(), sends the message, and sets published=1.

When a delayed message is received by the server, it puts it in the cache with timestamp=now()+delay. Then the server periodically (e.g., every second), checks the cache for any messages where timestamp<=now() && published==0 and sends them.

So, we cannot tell (with the current DB structure), which already-sent messages had delays and which were sent immediately upon receipt.

(I originally thought differentiating between past messages that had delays and those that didn't was necessary for your implementation, but I don't think it is anymore. I think your column headers could be changed to "message in cache scheduled for future" and "no message in cache scheduled for future." So even though you weren't proposing differentiating past messages, I think explaining the ntfy cache and scheduled message architecture is beneficial for this discussion, so I left the explanation above).

However, I'm focusing on the upper-right cell of your table. Under your implementation, if I send a message now, since it will be in the cache for 12 hours, during that time, I cannot schedule a future message (with the same uid) since it would clear the current message. I don't think we want that. I think it should be possible to schedule a future message and keep the current revision visible. Especially since some people have a very long cache expiration time (e.g., 1+ year).

That's what I was trying to convey before.

<!-- gh-comment-id:2928149122 --> @wunter8 commented on GitHub (Jun 1, 2025): That part of your understanding is correct. Here's a bit more about the current ntfy architecture. All messages are stored in the same cache table. Messages have a `timestamp` and a `published` boolean. When a no-delay message is received by the server, it puts it in the cache with `timestamp=now()`, sends the message, and sets `published=1`. When a delayed message is received by the server, it puts it in the cache with `timestamp=now()+delay`. Then the server periodically (e.g., every second), checks the cache for any messages where `timestamp<=now() && published==0` and sends them. So, we cannot tell (with the current DB structure), which already-sent messages had delays and which were sent immediately upon receipt. (I originally thought differentiating between past messages that had delays and those that didn't was necessary for your implementation, but I don't think it is anymore. I think your column headers could be changed to "message in cache scheduled for future" and "no message in cache scheduled for future." So even though you weren't proposing differentiating past messages, I think explaining the ntfy cache and scheduled message architecture is beneficial for this discussion, so I left the explanation above). However, I'm focusing on the upper-right cell of your table. Under your implementation, if I send a message now, since it will be in the cache for 12 hours, during that time, I cannot schedule a future message (with the same `uid`) since it would clear the current message. I don't think we want that. I think it should be possible to schedule a future message and keep the current revision visible. Especially since some people have a very long cache expiration time (e.g., 1+ year). That's what I was trying to convey before.
Author
Owner

@wunter8 commented on GitHub (Jun 2, 2025):

While I was preparing the previous response, I realized that as soon as a message (e.g., revision 3 of uid:reminder) expires from the cache, when the next message with uid:reminder is received, we won't have any idea what revision this is/should be. Should we impose a rule that messages can only be updated while at least one revision is in the cache?

I think the alternative is to create a new DB table that forever tracks the revision counts for each topic/uid pairing (e.g., CREATE TABLE revisions(id primary key, topic TEXT, uid TEXT, revision INTEGER).

The revision is intended to help the client know which message is the most recent for a particular uid. Can we just rely on timestamps? I guess that wouldn't work if multiple messages/updates are sent in the same second.

If we don't know the current revision number, I think we'd have to default to 1 (or 0?). When the client receives that new message, it wouldn't know to overwrite revision 3 (for example) with the new message (version 1) which was received after version 3 expired from the server cache.

I guess we could inform all the clients about the server's cache duration and making them calculate "this was received after revision 3 timestamp+cache duration, so this new message is actually supposed to be revision 4 instead of revision 1." But I don't love that idea

<!-- gh-comment-id:2928177913 --> @wunter8 commented on GitHub (Jun 2, 2025): While I was preparing the previous response, I realized that as soon as a message (e.g., revision 3 of `uid:reminder`) expires from the cache, when the next message with `uid:reminder` is received, we won't have any idea what revision this is/should be. Should we impose a rule that messages can only be updated while at least one revision is in the cache? I think the alternative is to create a new DB table that forever tracks the revision counts for each `topic/uid` pairing (e.g., `CREATE TABLE revisions(id primary key, topic TEXT, uid TEXT, revision INTEGER`). The revision is intended to help the client know which message is the most recent for a particular `uid`. Can we just rely on timestamps? I guess that wouldn't work if multiple messages/updates are sent in the same second. If we don't know the current revision number, I think we'd have to default to 1 (or 0?). When the client receives that new message, it wouldn't know to overwrite revision 3 (for example) with the new message (version 1) which was received after version 3 expired from the server cache. I guess we could inform all the clients about the server's cache duration and making them calculate "this was received after revision 3 timestamp+cache duration, so this new message is actually supposed to be revision 4 instead of revision 1." But I don't love that idea
Author
Owner

@gbansaghi commented on GitHub (Jun 2, 2025):

Thanks for the explanation, that was very helpful! I'm seeing the problem now with my proposal, and I do agree that keeping the current revision visible would be useful. In this case, I think updates should affect the latest revision, regardless if it's scheduled or delivered. So a scheduled message can be updated while the last delivered revision remains visible. If the user wishes to update the currently displayed message, they can send an update with Delay: 0 which would cause the message to be delivered immediately. Sending another message with a nonzero delay would create a new scheduled revision, which could be update without affecting the currently displayed message (at least until the current time reaches the delivery timestamp, or the update changes the timestamp to <=now()).

Note: I'm assuming here that any headers missing from an update would mean "don't modify those values". I.e. if a server receives a message with, say, tag and actions, for a message uid that has tag, delay and actions, then it will change tag and actions, but leave delay as-is. If there's no revision for that uid on the server, it would create a message with tag and actions only. Is that the idea?

Should we impose a rule that messages can only be updated while at least one revision is in the cache?

I think that's an implementation detail that users should not have to worry about. If an update is posted for an uid for which all revisions expired, ntfy creates the message as if it were new, resetting revision to 1 (or 0). Clients can disregard messages where the revision is less than what they currently have for that uid, except if it's the initial value. In that case, assume the message being updated expired from the server, and update it.

The only issue I see with this approach is this: let's say the client has revision 5 for a message that expired from the server. The server receives two updates in quick succession, so revision (let's say) 1 and 2 are sent to the client. If those arrive out of order (can then?), the client will not update the message to revision 2 (because it's less than 5, but not the initial value), then update it to revision 1 (because it is the initial value).

<!-- gh-comment-id:2931595035 --> @gbansaghi commented on GitHub (Jun 2, 2025): Thanks for the explanation, that was very helpful! I'm seeing the problem now with my proposal, and I do agree that keeping the current revision visible would be useful. In this case, I think updates should affect the latest revision, regardless if it's scheduled or delivered. So a scheduled message can be updated while the last delivered revision remains visible. If the user wishes to update the currently displayed message, they can send an update with `Delay: 0` which would cause the message to be delivered immediately. Sending another message with a nonzero delay would create a new scheduled revision, which could be update without affecting the currently displayed message (at least until the current time reaches the delivery timestamp, or the update changes the timestamp to `<=now()`). Note: I'm assuming here that any headers missing from an update would mean "don't modify those values". I.e. if a server receives a message with, say, `tag` and `actions`, for a message `uid` that has `tag`, `delay` and `actions`, then it will change `tag` and `actions`, but leave `delay` as-is. If there's no revision for that `uid` on the server, it would create a message with `tag` and `actions` only. Is that the idea? > Should we impose a rule that messages can only be updated while at least one revision is in the cache? I think that's an implementation detail that users should not have to worry about. If an update is posted for an `uid` for which all revisions expired, ntfy creates the message as if it were new, resetting revision to 1 (or 0). Clients can disregard messages where the revision is less than what they currently have for that `uid`, _except_ if it's the initial value. In that case, assume the message being updated expired from the server, and update it. The only issue I see with this approach is this: let's say the client has revision 5 for a message that expired from the server. The server receives two updates in quick succession, so revision (let's say) 1 and 2 are sent to the client. If those arrive out of order (can then?), the client will not update the message to revision 2 (because it's less than 5, but not the initial value), then update it to revision 1 (because it is the initial value).
Author
Owner

@wunter8 commented on GitHub (Jun 2, 2025):

In general, I have been imagining each message standing alone, especially for normal (non-scheduled) updates. Each message will be its own entry in the database (for backwards compatibility), so we won't worry about merging fields or anything.

I think we'll do the same thing for scheduled messages. If the currently scheduled message has tags (or no tags) and you want to change that, you need to send a new message that has a delay with no tags (or with tags, respectively).

I think the only exception/nuance for that is with the delay field. I could imagine times when I set the delay with a first request, want to change the message content with a second request, but I want the time to "keep counting down" based on the first request. I don't want to have to calculate the new delay myself (as the user sending requests) based on the difference between the first request and the update request. That's why I proposed something like delay: 0, delay: -1, or delay: keep (or maybe delay: continue?).

In that case, I'm thinking the new scheduled message would be added to the DB, the delay field (timestamp) from the old scheduled message would be copied, and the old scheduled message would be deleted, without merging any more of their fields.

<!-- gh-comment-id:2932040811 --> @wunter8 commented on GitHub (Jun 2, 2025): In general, I have been imagining each message standing alone, especially for normal (non-scheduled) updates. Each message will be its own entry in the database (for backwards compatibility), so we won't worry about merging fields or anything. I think we'll do the same thing for scheduled messages. If the currently scheduled message has tags (or no tags) and you want to change that, you need to send a new message that has a delay with no tags (or with tags, respectively). I think the only exception/nuance for that is with the `delay` field. I could imagine times when I set the delay with a first request, want to change the message content with a second request, but I want the time to "keep counting down" based on the first request. I don't want to have to calculate the new delay myself (as the user sending requests) based on the difference between the first request and the update request. That's why I proposed something like `delay: 0`, `delay: -1`, or `delay: keep` (or maybe `delay: continue`?). In that case, I'm thinking the new scheduled message would be added to the DB, the delay field (`timestamp`) from the old scheduled message would be copied, and the old scheduled message would be deleted, without merging any more of their fields.
Author
Owner

@binwiederhier commented on GitHub (Jun 3, 2025):

I was reading through the conversation above and I was able to follow much of it, but not all. I noticed though that the conversation almost entirely revolves around schedules messages to support the "dead man's snitch" ticket, and less about the original "i wanna update/delete a message" ticket. It may be worth excluding scheduled messages for now and handle only non-scheduled message updates.

I will spend 10min crafting some sample requests in the ticket. I ultimately do want to include scheduled messages, but it's likely a <1% case, and it would benefit so many more people to just support the non-scheduled messages for now.

So here it goes:

New database schema

mid update_id time 🍪 message deleted
r3ejWFd0370p r3ejWFd0370p 1748910783000 hi false
qFEhZ6uCHzDS r3ejWFd0370p 1748911048123 Hi again false
kqVp81sqMJ1z r3ejWFd0370p 1748911465912 true

= new

🍪 = changed

(a) Publish new message

Request:

POST /mytopic
Host: ntfy.sh

hi

Alternative request:

POST /mytopic/r3ejWFd0370p
Host: ntfy.sh

hi

Alternative request:

POST /mytopic
Uid: r3ejWFd0370p
Host: ntfy.sh

hi

Response:

{
  "id": "r3ejWFd0370p",
  "time": 1748910783,
  "mtime": 1748910783000,
  "expires": 1748953983,
  "event": "message",
  "topic": "mytopic",
  "message": "hi"
}

Database after request:

mid update_id time message deleted
r3ejWFd0370p r3ejWFd0370p 1748910783000 hi false

(b) Update this message:

Request:

POST /mytopic/r3ejWFd0370p
Host: ntfy.sh

Hi again

Alternative request:

POST /mytopic
Uid: r3ejWFd0370p
Host: ntfy.sh

Hi again

Response:

{
  "id": "qFEhZ6uCHzDS",
  "update_id": "r3ejWFd0370p",
  "time": 1748911048,
  "mtime": 1748911048000,
  "expires": 1748954248,
  "event": "message",
  "topic": "mytopic",
  "message": "Hi again"
}

Database after request:

mid update_id time message deleted
r3ejWFd0370p r3ejWFd0370p 1748910783000 hi false
qFEhZ6uCHzDS r3ejWFd0370p 1748911048123 Hi again false

(c) Delete this message:

Request:

DELETE /mytopic/r3ejWFd0370p
Host: ntfy.sh

Alternative request:

DELETE /mytopic
Uid: r3ejWFd0370p
Host: ntfy.sh

Response:

{
  "id": "kqVp81sqMJ1z",
  "update_id": "r3ejWFd0370p",
  "deleted": true,
  "time": 1748911465,
  "mtime": 1748911465912,
  "expires": 1748954665,
  "event": "message",
  "topic": "mytopic",
  "message": "hi"
}

Database after request:

mid update_id time message deleted
r3ejWFd0370p r3ejWFd0370p 1748910783000 hi false
qFEhZ6uCHzDS r3ejWFd0370p 1748911048123 Hi again false
kqVp81sqMJ1z r3ejWFd0370p 1748911465912 true

Additional thoughts:

  • What happens if all revisions for a uid got pruned from the cache? Do we start over at 1?
  • We could ditch the "revision" and just use the time as revision 🤔
<!-- gh-comment-id:2933005622 --> @binwiederhier commented on GitHub (Jun 3, 2025): I was reading through the conversation above and I was able to follow much of it, but not all. I noticed though that the conversation almost entirely revolves around schedules messages to support the "dead man's snitch" ticket, and less about the original "i wanna update/delete a message" ticket. It may be worth excluding scheduled messages for now and handle only non-scheduled message updates. I will spend 10min crafting some sample requests in the ticket. I ultimately do want to include scheduled messages, but it's likely a <1% case, and it would benefit so many more people to just support the non-scheduled messages for now. So here it goes: **New database schema** | mid | update_id ⭐ | time 🍪 | message | deleted ⭐ | |-------|-------|------------|----------|-----------| |r3ejWFd0370p | r3ejWFd0370p | 1748910783**000** | hi | false | |qFEhZ6uCHzDS | r3ejWFd0370p | 1748911048**123** | Hi again | false | |kqVp81sqMJ1z | r3ejWFd0370p | 1748911465**912** | | true | ⭐ = new 🍪 = changed **(a) Publish new message** Request: ``` POST /mytopic Host: ntfy.sh hi ``` Alternative request: ``` POST /mytopic/r3ejWFd0370p Host: ntfy.sh hi ``` Alternative request: ``` POST /mytopic Uid: r3ejWFd0370p Host: ntfy.sh hi ``` Response: ``` { "id": "r3ejWFd0370p", "time": 1748910783, "mtime": 1748910783000, "expires": 1748953983, "event": "message", "topic": "mytopic", "message": "hi" } ``` Database after request: | mid | update_id | time | message | deleted | |-------|-------|------------|----------|-----------| |r3ejWFd0370p | r3ejWFd0370p | 1748910783000 | hi | false | **(b) Update this message:** Request: ``` POST /mytopic/r3ejWFd0370p Host: ntfy.sh Hi again ``` Alternative request: ``` POST /mytopic Uid: r3ejWFd0370p Host: ntfy.sh Hi again ``` Response: ``` { "id": "qFEhZ6uCHzDS", "update_id": "r3ejWFd0370p", "time": 1748911048, "mtime": 1748911048000, "expires": 1748954248, "event": "message", "topic": "mytopic", "message": "Hi again" } ``` Database after request: | mid | update_id | time | message | deleted | |-------|-------|------------|----------|-----------| |r3ejWFd0370p | r3ejWFd0370p | 1748910783000 | hi | false | |qFEhZ6uCHzDS | r3ejWFd0370p | 1748911048123 | Hi again | false | **(c) Delete this message:** Request: ``` DELETE /mytopic/r3ejWFd0370p Host: ntfy.sh ``` Alternative request: ``` DELETE /mytopic Uid: r3ejWFd0370p Host: ntfy.sh ``` Response: ``` { "id": "kqVp81sqMJ1z", "update_id": "r3ejWFd0370p", "deleted": true, "time": 1748911465, "mtime": 1748911465912, "expires": 1748954665, "event": "message", "topic": "mytopic", "message": "hi" } ``` Database after request: | mid | update_id | time | message | deleted | |-------|-------|------------|----------|-----------| |r3ejWFd0370p | r3ejWFd0370p | 1748910783000 | hi | false | |qFEhZ6uCHzDS | r3ejWFd0370p | 1748911048123 | Hi again | false | |kqVp81sqMJ1z | r3ejWFd0370p | 1748911465912 | | true | --- Additional thoughts: - What happens if all revisions for a `uid` got pruned from the cache? Do we start over at 1? - We could ditch the "revision" and just use the `time` as revision 🤔
Author
Owner

@wunter8 commented on GitHub (Jun 3, 2025):

  1. Would the response to the DELETE request include a value in the message field or just an empty string?
    a. If it does have a value, it'd be the value of the most recent revision (revision 2 in this case) and not the value of revision 1 like you example above, right?
  2. What do you think about the above proposed GET /topic/uid/delete endpoint to accommodate services that can only issue GET/webhook requests?
  3. Do you want an explicit deleted field vs my proposed empty message field on its own?
    a. If we do add a deleted column to the message cache table, I think it'd be better to call it cleared since we're just clearing it from the active notifications on the client device vs deleting it from the client device or deleting it from the server.
  4. I think we don't need a revision field if we add an mtime timestamp field that includes milliseconds
<!-- gh-comment-id:2933051622 --> @wunter8 commented on GitHub (Jun 3, 2025): 1. Would the response to the DELETE request include a value in the message field or just an empty string? a. If it does have a value, it'd be the value of the most recent revision (revision 2 in this case) and not the value of revision 1 like you example above, right? 2. What do you think about the above proposed `GET /topic/uid/delete` endpoint to accommodate services that can only issue GET/webhook requests? 3. Do you want an explicit `deleted` field vs my proposed empty message field on its own? a. If we do add a deleted column to the message cache table, I think it'd be better to call it `cleared` since we're just clearing it from the active notifications on the client device vs deleting it from the client device or deleting it from the server. 4. I think we don't need a revision field if we add an `mtime` timestamp field that includes milliseconds
Author
Owner

@wunter8 commented on GitHub (Jun 3, 2025):

Would the GET request for publishing a message with a uid have the following format?

GET /mytopic/publish?message=hi&uid=r3ejWFd0370p
<!-- gh-comment-id:2933062256 --> @wunter8 commented on GitHub (Jun 3, 2025): Would the GET request for publishing a message with a uid have the following format? ``` GET /mytopic/publish?message=hi&uid=r3ejWFd0370p ```
Author
Owner

@binwiederhier commented on GitHub (Jun 3, 2025):

Would the response to the DELETE request include a value in the message field or just an empty string?

Just an empty string. I will update the example above.

What do you think about the above proposed GET /topic/uid/delete endpoint to accommodate services that can only issue GET/webhook requests?

I am not opposed, but I would not conflate optimizations or convenience methods like this with the original feature. Baby steps. The impl won't be easy anyway, and it must be supported on Android, web app and iOS (eventually).

Do you want an explicit deleted field vs my proposed empty message field on its own?

I think an explicit deleted flag is better. I still like the name deleted more than cleared.

I think we don't need a revision field if we add an mtime timestamp field that includes milliseconds

👍

--

I will update the example above.

I also do not like the name uid. Maybe sequence_id?! Idk. We can change the name later.

<!-- gh-comment-id:2933101952 --> @binwiederhier commented on GitHub (Jun 3, 2025): > Would the response to the DELETE request include a value in the message field or just an empty string? Just an empty string. I will update the example above. > What do you think about the above proposed GET /topic/uid/delete endpoint to accommodate services that can only issue GET/webhook requests? I am not opposed, but I would not conflate optimizations or convenience methods like this with the original feature. Baby steps. The impl won't be easy anyway, and it must be supported on Android, web app and iOS (eventually). > Do you want an explicit deleted field vs my proposed empty message field on its own? I think an explicit `deleted` flag is better. I still like the name `deleted` more than `cleared`. > I think we don't need a revision field if we add an mtime timestamp field that includes milliseconds 👍 -- I will update the example above. I also do not like the name `uid`. Maybe `sequence_id`?! Idk. We can change the name later.
Author
Owner

@wunter8 commented on GitHub (Jun 3, 2025):

and it must be supported on Android, web app and iOS (eventually).

Updating/deleting a message needs to be possible from those devices? Or just displaying a message update/deleting an existing notification needs to be supported?

<!-- gh-comment-id:2933115665 --> @wunter8 commented on GitHub (Jun 3, 2025): > and it must be supported on Android, web app and iOS (eventually). Updating/deleting a message needs to be possible from those devices? Or just displaying a message update/deleting an existing notification needs to be supported?
Author
Owner

@binwiederhier commented on GitHub (Jun 3, 2025):

Just displaying I meant

<!-- gh-comment-id:2933144276 --> @binwiederhier commented on GitHub (Jun 3, 2025): Just displaying I meant
Author
Owner

@pnxs commented on GitHub (Jun 3, 2025):

It's nice to see the progress here.

As far as I can see, a client (displaying device) would subscribe to a topic.
For every received message, it stores the latest mtime of the uid.
If a connection disruption occurs, the client uses the mtime (which acts like a version for the uid) to receive all newer updates (including deletions).
It's also possible to track the history of message-updates on the client - for example, in case of short-lived notifications or missed notifications that are deleted because they are no longer relevant. This way the user would still be able to check whether some of these had occurred. I like this.

As for the question about uid. To me, the existing concept of notifications forms the foundation for the new concept of changeable notifications. As I understand it - for backward compatibility - all updates to changeable notifications will be transmitted as a sequence of notifications. On one hand sequence_id may be a fitting name, as it describes the idea of changeable notifications as a sequence of notifications. However, perhaps we could find a name, that better describes this new concept. I must admit (English isn't my main language), it's difficult. I thought about the german words "Nachricht" and "Meldung". Where the former has the character of a single notification and the latter conveys more the meaning of the reporting or presenting part. Yeah, but sequence_id or thread_id sounds not to bad.

<!-- gh-comment-id:2934054651 --> @pnxs commented on GitHub (Jun 3, 2025): It's nice to see the progress here. As far as I can see, a client (displaying device) would subscribe to a topic. For every received message, it stores the latest `mtime` of the `uid`. If a connection disruption occurs, the client uses the `mtime` (which acts like a version for the `uid`) to receive all newer updates (including deletions). It's also possible to track the history of message-updates on the client - for example, in case of short-lived notifications or missed notifications that are deleted because they are no longer relevant. This way the user would still be able to check whether some of these had occurred. I like this. As for the question about `uid`. To me, the existing concept of `notifications` forms the foundation for the new concept of `changeable notifications`. As I understand it - for backward compatibility - all updates to `changeable notifications` will be transmitted as a sequence of `notifications`. On one hand `sequence_id` may be a fitting name, as it describes the idea of `changeable notifications` as a sequence of notifications. However, perhaps we could find a name, that better describes this new concept. I must admit (English isn't my main language), it's difficult. I thought about the german words "Nachricht" and "Meldung". Where the former has the character of a single notification and the latter conveys more the meaning of the reporting or presenting part. Yeah, but `sequence_id` or `thread_id` sounds not to bad.
Author
Owner

@wunter8 commented on GitHub (Jun 3, 2025):

Alternative request:

POST /mytopic/r3ejWFd0370p
Host: ntfy.sh

hi

@binwiederhier Do you want to support this format and therefore exclude (ws|json|sse|raw|publish|send|trigger) as possible uids?

<!-- gh-comment-id:2937778619 --> @wunter8 commented on GitHub (Jun 3, 2025): > Alternative request: > > POST /mytopic/r3ejWFd0370p > Host: ntfy.sh > > hi @binwiederhier Do you want to support this format and therefore exclude `(ws|json|sse|raw|publish|send|trigger)` as possible `uid`s?
Author
Owner

@binwiederhier commented on GitHub (Jun 4, 2025):

I think it is the RESTy way to do it, so yeah, let's do that. We may want to require a minimum length for the update_id anyway, but blacklisting the above is not a bad idea

<!-- gh-comment-id:2940369371 --> @binwiederhier commented on GitHub (Jun 4, 2025): I think it is the RESTy way to do it, so yeah, let's do that. We may want to require a minimum length for the update_id anyway, but blacklisting the above is not a bad idea
Author
Owner

@grexe commented on GitHub (Jun 9, 2025):

discovered this wonderful project only now and already love it!
Stumbled across the need for this feature as well (need to keep track of changes of files on my NAS and only keep the latest state/path).

Talking about the RESTy way, I would really prefer to use PUT for updating messages instead of another POST, which is usually only used for creating new resources. This was suggested by @blacklight here back then:
https://github.com/binwiederhier/ntfy/issues/303#issuecomment-1144286256

Can't wait to test this! Running on my TrueNAS, setup was a breeze and everything worked out of the box!

<!-- gh-comment-id:2955288764 --> @grexe commented on GitHub (Jun 9, 2025): discovered this wonderful project only now and already love it! Stumbled across the need for this feature as well (need to keep track of changes of files on my NAS and only keep the latest state/path). Talking about the RESTy way, I would really prefer to use `PUT` for updating messages instead of another `POST`, which is usually only used for creating new resources. This was suggested by @blacklight here back then: https://github.com/binwiederhier/ntfy/issues/303#issuecomment-1144286256 Can't wait to test this! Running on my TrueNAS, setup was a breeze and everything worked out of the box!
Author
Owner

@wcypierre commented on GitHub (Sep 2, 2025):

Looking forward for this to be implemented :D

<!-- gh-comment-id:3247175375 --> @wcypierre commented on GitHub (Sep 2, 2025): Looking forward for this to be implemented :D
Author
Owner

@wunter8 commented on GitHub (Oct 18, 2025):

This is being worked on here: https://github.com/binwiederhier/ntfy/pull/1466

<!-- gh-comment-id:3417792910 --> @wunter8 commented on GitHub (Oct 18, 2025): This is being worked on here: https://github.com/binwiederhier/ntfy/pull/1466
Author
Owner

@binwiederhier commented on GitHub (Jan 8, 2026):

This is the current state of affairs for this:

https://github.com/user-attachments/assets/d5a758fd-dea5-44e3-a54b-c15347ac826c

The API is like this:

Create message

curl -d "created" localhost:2586/test6
{"id":"VXsqkDO57Gaz","time":1767910451,"expires":1773094451,"event":"message","topic":"test6","message":"created"}

curl -d "created" localhost:2586/test6/custom_seq
{"id":"FW5CZsSMDC8x","sequence_id":"custom_seq","time":1767910566,"expires":1773094566,"event":"message","topic":"test6","message":"created"}

Update message

curl -d "updated" localhost:2586/test6/VXsqkDO57Gaz
{"id":"omBT5Z93IKCf","sequence_id":"VXsqkDO57Gaz","time":1767910598,"expires":1773094598,"event":"message","topic":"test6","message":"updated"}

curl -d "updated" localhost:2586/test6/custom_seq  
{"id":"KR9KcSzWCmxu","sequence_id":"custom_seq","time":1767910616,"expires":1773094616,"event":"message","topic":"test6","message":"updated"}

Delete message

curl -X DELETE localhost:2586/test6/VXsqkDO57Gaz
{"id":"sHO7vyUnRVvn","sequence_id":"VXsqkDO57Gaz","time":1767910649,"expires":1773094649,"event":"message","topic":"test6","message":"deleted","deleted":true}

curl -X DELETE localhost:2586/test6/custom_seq  
{"id":"TshjBnlt0pTN","sequence_id":"custom_seq","time":1767910666,"expires":1773094666,"event":"message","topic":"test6","message":"deleted","deleted":true}

Subscribers/pollers

curl "localhost:2586/test6/json?poll=1"
{"id":"VXsqkDO57Gaz","time":1767910451,"expires":1773094451,"event":"message","topic":"test6","message":"created"}
{"id":"FW5CZsSMDC8x","sequence_id":"custom_seq","time":1767910566,"expires":1773094566,"event":"message","topic":"test6","message":"created"}
{"id":"omBT5Z93IKCf","sequence_id":"VXsqkDO57Gaz","time":1767910598,"expires":1773094598,"event":"message","topic":"test6","message":"updated"}
{"id":"KR9KcSzWCmxu","sequence_id":"custom_seq","time":1767910616,"expires":1773094616,"event":"message","topic":"test6","message":"updated"}
{"id":"sHO7vyUnRVvn","sequence_id":"VXsqkDO57Gaz","time":1767910649,"expires":1773094649,"event":"message","topic":"test6","message":"deleted","deleted":true}
{"id":"TshjBnlt0pTN","sequence_id":"custom_seq","time":1767910666,"expires":1773094666,"event":"message","topic":"test6","message":"deleted","deleted":true}

Storage

Server:

  • Each message has a sequence ID. If the sequence ID not set when publishing, we set sequenceID = messageID
  • Each update is its own message with a sequence_id set
  • Each deletion is its own message with deleted: true

Web app:

  • When a new message arrives, old messages in the sequence are hard deleted from the local storage
  • The latest message is stored and displayed.
  • The existing browser notification is updated.
  • If the latest message is a deleted:true message, no message is added to the local storage. The browser notification cannot be deleted.

Android app:

  • When a new message arrives, old messages in the sequence are SOFT deleted from the local storage
  • The latest message is stored and displayed.
  • The existing browser notification is updated.
  • If the latest message is a deleted:true message, no message is added to the local storage. The Android popup notification is canceled (deleted)
<!-- gh-comment-id:3726083165 --> @binwiederhier commented on GitHub (Jan 8, 2026): This is the current state of affairs for this: https://github.com/user-attachments/assets/d5a758fd-dea5-44e3-a54b-c15347ac826c The API is like this: # Create message ``` curl -d "created" localhost:2586/test6 {"id":"VXsqkDO57Gaz","time":1767910451,"expires":1773094451,"event":"message","topic":"test6","message":"created"} curl -d "created" localhost:2586/test6/custom_seq {"id":"FW5CZsSMDC8x","sequence_id":"custom_seq","time":1767910566,"expires":1773094566,"event":"message","topic":"test6","message":"created"} ``` # Update message ``` curl -d "updated" localhost:2586/test6/VXsqkDO57Gaz {"id":"omBT5Z93IKCf","sequence_id":"VXsqkDO57Gaz","time":1767910598,"expires":1773094598,"event":"message","topic":"test6","message":"updated"} curl -d "updated" localhost:2586/test6/custom_seq {"id":"KR9KcSzWCmxu","sequence_id":"custom_seq","time":1767910616,"expires":1773094616,"event":"message","topic":"test6","message":"updated"} ``` # Delete message ``` curl -X DELETE localhost:2586/test6/VXsqkDO57Gaz {"id":"sHO7vyUnRVvn","sequence_id":"VXsqkDO57Gaz","time":1767910649,"expires":1773094649,"event":"message","topic":"test6","message":"deleted","deleted":true} curl -X DELETE localhost:2586/test6/custom_seq {"id":"TshjBnlt0pTN","sequence_id":"custom_seq","time":1767910666,"expires":1773094666,"event":"message","topic":"test6","message":"deleted","deleted":true} ``` # Subscribers/pollers ``` curl "localhost:2586/test6/json?poll=1" {"id":"VXsqkDO57Gaz","time":1767910451,"expires":1773094451,"event":"message","topic":"test6","message":"created"} {"id":"FW5CZsSMDC8x","sequence_id":"custom_seq","time":1767910566,"expires":1773094566,"event":"message","topic":"test6","message":"created"} {"id":"omBT5Z93IKCf","sequence_id":"VXsqkDO57Gaz","time":1767910598,"expires":1773094598,"event":"message","topic":"test6","message":"updated"} {"id":"KR9KcSzWCmxu","sequence_id":"custom_seq","time":1767910616,"expires":1773094616,"event":"message","topic":"test6","message":"updated"} {"id":"sHO7vyUnRVvn","sequence_id":"VXsqkDO57Gaz","time":1767910649,"expires":1773094649,"event":"message","topic":"test6","message":"deleted","deleted":true} {"id":"TshjBnlt0pTN","sequence_id":"custom_seq","time":1767910666,"expires":1773094666,"event":"message","topic":"test6","message":"deleted","deleted":true} ``` # Storage Server: - Each message has a sequence ID. If the sequence ID not set when publishing, we set sequenceID = messageID - Each update is its own message with a `sequence_id` set - Each deletion is its own message with `deleted: true` Web app: - When a new message arrives, old messages in the sequence are hard deleted from the local storage - The latest message is stored and displayed. - The existing browser notification is updated. - If the latest message is a deleted:true message, no message is added to the local storage. The browser notification cannot be deleted. Android app: - When a new message arrives, old messages in the sequence are SOFT deleted from the local storage - The latest message is stored and displayed. - The existing browser notification is updated. - If the latest message is a deleted:true message, no message is added to the local storage. The Android popup notification is canceled (deleted)
Author
Owner

@binwiederhier commented on GitHub (Jan 8, 2026):

Questions:

  • Should DELETE /<topic>/<sequence-id> delete the notification on the client or mark it as read
  • What if DELETE /<topic>/<sequence-id> deletes it and PUT /<topic>/sequence-id>/read marks it as read?
  • What if "deleted" and "read" events are not messages, but their own events, like so:
{"id":"VXsqkDO57Gaz","event":"message_read","sequence_id":"FW5CZsSMDC8x"}
{"id":"VXsqkDO57Gaz","event":"message_delete","sequence_id":"FW5CZsSMDC8x"}
<!-- gh-comment-id:3726090123 --> @binwiederhier commented on GitHub (Jan 8, 2026): Questions: - Should `DELETE /<topic>/<sequence-id>` delete the notification on the client or mark it as read - What if `DELETE /<topic>/<sequence-id>` deletes it and `PUT /<topic>/sequence-id>/read` marks it as read? - What if "deleted" and "read" events are not messages, but their own events, like so: ``` {"id":"VXsqkDO57Gaz","event":"message_read","sequence_id":"FW5CZsSMDC8x"} {"id":"VXsqkDO57Gaz","event":"message_delete","sequence_id":"FW5CZsSMDC8x"} ```
Author
Owner

@wunter8 commented on GitHub (Jan 8, 2026):

Why the difference between hard delete and soft delete in the web app and Android app?

Each message has a sequence ID. If the sequence ID not set when publishing, we set sequenceID = messageID

The example you showed doesn't have a sequence_id ({"id":"VXsqkDO57Gaz","time":1767910451,"expires":1773094451,"event":"message","topic":"test6","message":"created"})

I always imagined DELETE would dismiss an existing/visible notification (so remove a notification from my notification shade, for example). I don't think I want it to be removed inside the app because I'd like to be able to see it there for history/logging reasons.

So, if by "mark as read" you mean "remove a notification from the notification shade or no-op if I've already dismissed that notification from the shade", that's what I would prefer.

As far as the API, having both options (delete and "mark as read") would probably be nice so those that don't care about keeping a history can delete it entirely.

I'm not sure what introducing those new event types would do. It'd probably be fine. And I think it's a more accurate representation of what's happening vs sending a new message. Especially if we have "mark as read" (or "cleared"?) and "deleted". I think it'd be difficult to represent both of those inside our current "message" event

<!-- gh-comment-id:3726169310 --> @wunter8 commented on GitHub (Jan 8, 2026): Why the difference between hard delete and soft delete in the web app and Android app? > Each message has a sequence ID. If the sequence ID not set when publishing, we set sequenceID = messageID The example you showed doesn't have a sequence_id (`{"id":"VXsqkDO57Gaz","time":1767910451,"expires":1773094451,"event":"message","topic":"test6","message":"created"}`) I always imagined DELETE would dismiss an existing/visible notification (so remove a notification from my notification shade, for example). I don't think I want it to be removed inside the app because I'd like to be able to see it there for history/logging reasons. So, if by "mark as read" you mean "remove a notification from the notification shade or no-op if I've already dismissed that notification from the shade", that's what I would prefer. As far as the API, having both options (delete and "mark as read") would probably be nice so those that don't care about keeping a history can delete it entirely. I'm not sure what introducing those new event types would do. It'd probably be fine. And I think it's a more accurate representation of what's happening vs sending a new message. Especially if we have "mark as read" (or "cleared"?) and "deleted". I think it'd be difficult to represent both of those inside our current "message" event
Author
Owner

@binwiederhier commented on GitHub (Jan 9, 2026):

Why the difference between hard delete and soft delete in the web app and Android app?

The Android app already does soft deletes. Originally this was so it can clean up the attachments separately. I am not sure if it ever hard deletes the entries, which is a problem. But I didn't want to solve too many problems at once.

As far as the API, having both options (delete and "mark as read") would probably be nice so those that don't care about keeping a history can delete it entirely.

Yeah it seems marginally more work to do both, so I'll probably do both.

The example you showed doesn't have a sequence_id

I don't want to change the original message structure, so only message 2, 3, 4, ... of a sequence will show the sequence_id.

<!-- gh-comment-id:3726662051 --> @binwiederhier commented on GitHub (Jan 9, 2026): > Why the difference between hard delete and soft delete in the web app and Android app? The Android app already does soft deletes. Originally this was so it can clean up the attachments separately. I am not sure if it ever hard deletes the entries, which is a problem. But I didn't want to solve too many problems at once. > As far as the API, having both options (delete and "mark as read") would probably be nice so those that don't care about keeping a history can delete it entirely. Yeah it seems marginally more work to do both, so I'll probably do both. > The example you showed doesn't have a sequence_id I don't want to change the original message structure, so only message 2, 3, 4, ... of a sequence will show the sequence_id.
Author
Owner

@wunter8 commented on GitHub (Jan 9, 2026):

I was thinking soft deletes would be preferred in both clients. So you can see the revision history. But that's probably not necessary. Just my paranoid "save all the data because I might need it at some point!" 😅

<!-- gh-comment-id:3726686662 --> @wunter8 commented on GitHub (Jan 9, 2026): I was thinking soft deletes would be preferred in both clients. So you can see the revision history. But that's probably not necessary. Just my paranoid "save all the data because I might need it at some point!" 😅
Author
Owner

@binwiederhier commented on GitHub (Jan 9, 2026):

I think it may be cool to see the history of a message at one point, but not even Slack or Discord do that. How the clients handle the protocol can be adjusted later. For now it's important to get the protocol right

<!-- gh-comment-id:3726696753 --> @binwiederhier commented on GitHub (Jan 9, 2026): I think it may be cool to see the history of a message at one point, but not even Slack or Discord do that. How the clients handle the protocol can be adjusted later. For now it's important to get the protocol right
Author
Owner

@binwiederhier commented on GitHub (Jan 9, 2026):

Okay, I've hit a pickle / dead end: Marking a notification as "read" aka "clearing it", or even "deleting it" (swiping in the Android app) can now be triggered via the API with DELETE/PUT as described above, but it does not sync state between clients.

A swipe in the Android app should trigger a DELETE /topic/sid, which would delete it in the web app. That sounds fine until you think of bulk actions or the fact that the deleteAfter in the Android app and web app are different. I guess I could allow bulk actions for deleting and mark as read. Thinking of the rate limits and such, people would surely not like that deleting and marking as read would double/triple the message count.

ntfy doesn't keep the entire state on the server. Not all messages are there after 12h, so the state can differ between clients...

Ahh

<!-- gh-comment-id:3726747379 --> @binwiederhier commented on GitHub (Jan 9, 2026): Okay, I've hit a pickle / dead end: Marking a notification as "read" aka "clearing it", or even "deleting it" (swiping in the Android app) can now be triggered via the API with DELETE/PUT as described above, but it does not sync state between clients. A swipe in the Android app should trigger a `DELETE /topic/sid`, which would delete it in the web app. That sounds fine until you think of bulk actions or the fact that the deleteAfter in the Android app and web app are different. I guess I could allow bulk actions for deleting and mark as read. Thinking of the rate limits and such, people would surely not like that deleting and marking as read would double/triple the message count. ntfy doesn't keep the entire state on the server. Not all messages are there after 12h, so the state can differ between clients... Ahh
Author
Owner

@capi commented on GitHub (Jan 9, 2026):

I think your latest point adds basically a new use-case, which is state synchronization between the clients. For what I personally am looking forward to this feature for, is that I want to cancel/remove pending notifications from the sender side, since the notification/pending request has already been handled by a different client or the state became obsolete on the sender side. This seems to be very well covered with your current proposal and the current state.
Maybe just consider the client synchronization a separate feature to be solved independently and later.

<!-- gh-comment-id:3727453806 --> @capi commented on GitHub (Jan 9, 2026): I think your latest point adds basically a new use-case, which is state synchronization between the clients. For what I personally am looking forward to this feature for, is that I want to cancel/remove pending notifications from the sender side, since the notification/pending request has already been handled by a different client or the state became obsolete on the sender side. This seems to be very well covered with your current proposal and the current state. Maybe just consider the client synchronization a separate feature to be solved independently and later.
Author
Owner

@wunter8 commented on GitHub (Jan 10, 2026):

but it does not sync state between clients.

I think that's fine for now. It could be added later. And the comment about the server not keeping all the state, I think that's also fine.

ntfy doesn't work well in general if devices are disconnected for longer than the cache time. So it's expected that state and sync and stuff might be negatively impacted if devices are offline longer than the cache duration

<!-- gh-comment-id:3731352923 --> @wunter8 commented on GitHub (Jan 10, 2026): > but it does not sync state between clients. I think that's fine for now. It could be added later. And the comment about the server not keeping all the state, I think that's also fine. ntfy doesn't work well in general if devices are disconnected for longer than the cache time. So it's expected that state and sync and stuff might be negatively impacted if devices are offline longer than the cache duration
Author
Owner

@binwiederhier commented on GitHub (Jan 14, 2026):

I am still polishing, but here's the state of affairs:

https://github.com/user-attachments/assets/597e8dae-81a0-4706-b22b-b001d717a089

Create / update / delete / clear message

  • Update = add new message with matching sequence_id (delete old messages with same sequence IDs from clients)
  • Delete = delete message sequence from client(s)
  • Clear = hide popup notification in clients, and mark as read
#!/bin/bash
set -x

topic=test103
sleep=3
seqA=seq$RANDOM
seqB=seq$RANDOM

# Create new message with chosen sequence A
curl -u phil:phil -d "created seqA=$seqA" localhost:2586/$topic/$seqA
sleep $sleep

# Create new message with chosen sequence B
curl -u phil:phil -d "created seqB=$seqB" localhost:2586/$topic/$seqB
sleep $sleep

# Update sequence A (moves above B)
curl -u phil:phil -d "updated seqA=$seqA" localhost:2586/$topic/$seqA
sleep $sleep

# Update sequence B (B moves up again
curl -u phil:phil -d "updated seqB=$seqB" localhost:2586/$topic/$seqB
sleep $sleep

# Mark A read (= clears notification in Android)
curl -u phil:phil -X PUT localhost:2586/$topic/$seqA/clear
sleep $sleep

# Mark B read (= clears notification in Android)
curl -u phil:phil -X PUT localhost:2586/$topic/$seqB/clear
sleep $sleep

# Delete A
curl -u phil:phil -X DELETE localhost:2586/$topic/$seqA
sleep $sleep

# Delete B
curl -u phil:phil -X DELETE localhost:2586/$topic/$seqB
sleep $sleep

# Revive B
curl -u phil:phil -d "revived seqB=$seqB" localhost:2586/$topic/$seqB
sleep $sleep

# Delete B again
curl -u phil:phil -X DELETE localhost:2586/$topic/$seqB

Subscribers/pollers

curl -u phil:phil "http://localhost:2586/test103/json?poll=1"
{"id":"PhZhyDlKwdhT","sequence_id":"seq15819","time":1768353449,"expires":1773537449,"event":"message","topic":"test103","message":"created seqA=seq15819"}
{"id":"5Mxo5LSu2P8N","sequence_id":"seq21256","time":1768353452,"expires":1773537452,"event":"message","topic":"test103","message":"created seqB=seq21256"}
{"id":"Wh4u0rSfBmPX","sequence_id":"seq15819","time":1768353455,"expires":1773537455,"event":"message","topic":"test103","message":"updated seqA=seq15819"}
{"id":"MLqJJf4xFv18","sequence_id":"seq21256","time":1768353458,"expires":1773537458,"event":"message","topic":"test103","message":"updated seqB=seq21256"}
{"id":"cRYDwQYZjOzm","sequence_id":"seq15819","time":1768353461,"expires":1773537461,"event":"message_clear","topic":"test103"}
{"id":"0NkR9VR2AGGt","sequence_id":"seq21256","time":1768353464,"expires":1773537464,"event":"message_clear","topic":"test103"}
{"id":"qWd8WpK2eTpm","sequence_id":"seq15819","time":1768353467,"expires":1773537467,"event":"message_delete","topic":"test103"}
{"id":"M0kEPhZHLV4H","sequence_id":"seq21256","time":1768353470,"expires":1773537470,"event":"message_delete","topic":"test103"}
{"id":"BggTMOpN5YHe","sequence_id":"seq21256","time":1768353473,"expires":1773537473,"event":"message","topic":"test103","message":"revived seqB=seq21256"}
{"id":"j5FdVMJhIC5A","sequence_id":"seq21256","time":1768353476,"expires":1773537476,"event":"message_delete","topic":"test103"}

Apps

Server:

  • Each message has a sequence ID. If the sequence ID not set when publishing, we set sequenceID = messageID
  • If sequenceID = messageID, we do NOT output it when polling or after publishing
  • Each update is its own message with a sequence_id set
  • Each clear (=mark read) event is its own message with event=message_clear
  • Each deletion event is its own message with event=message_delete

Web app:

  • When a new message arrives, old messages in the sequence are hard deleted from the local storage
  • The latest message is stored and displayed.
  • The existing browser notification is updated.
  • If the latest message is a event=message_delete message, no message is added to the local storage. The browser notification cannot be deleted.
  • If the latest message is a event=message_clear message, the message is marked as read in the local storage. The icon badge number is decreased. The browser popup notification cannot be reliably canceled.

Android app:

  • When a new message arrives, old messages in the sequence are soft-deleted from the local storage
  • The latest message is stored and displayed.
  • The existing browser notification is updated or cleared
  • If the latest message is a event=message_delete message, no message is added to the local storage. The Android popup notification is canceled (cleared)
  • If the latest message is a event=message_clear message, the message is marked as read in the local storage. The icon badge number is decreased.
<!-- gh-comment-id:3747306715 --> @binwiederhier commented on GitHub (Jan 14, 2026): I am still polishing, but here's the state of affairs: https://github.com/user-attachments/assets/597e8dae-81a0-4706-b22b-b001d717a089 # Create / update / delete / clear message - Update = add new message with matching `sequence_id` (delete old messages with same sequence IDs from clients) - Delete = delete message sequence from client(s) - Clear = hide popup notification in clients, and mark as read ``` #!/bin/bash set -x topic=test103 sleep=3 seqA=seq$RANDOM seqB=seq$RANDOM # Create new message with chosen sequence A curl -u phil:phil -d "created seqA=$seqA" localhost:2586/$topic/$seqA sleep $sleep # Create new message with chosen sequence B curl -u phil:phil -d "created seqB=$seqB" localhost:2586/$topic/$seqB sleep $sleep # Update sequence A (moves above B) curl -u phil:phil -d "updated seqA=$seqA" localhost:2586/$topic/$seqA sleep $sleep # Update sequence B (B moves up again curl -u phil:phil -d "updated seqB=$seqB" localhost:2586/$topic/$seqB sleep $sleep # Mark A read (= clears notification in Android) curl -u phil:phil -X PUT localhost:2586/$topic/$seqA/clear sleep $sleep # Mark B read (= clears notification in Android) curl -u phil:phil -X PUT localhost:2586/$topic/$seqB/clear sleep $sleep # Delete A curl -u phil:phil -X DELETE localhost:2586/$topic/$seqA sleep $sleep # Delete B curl -u phil:phil -X DELETE localhost:2586/$topic/$seqB sleep $sleep # Revive B curl -u phil:phil -d "revived seqB=$seqB" localhost:2586/$topic/$seqB sleep $sleep # Delete B again curl -u phil:phil -X DELETE localhost:2586/$topic/$seqB ``` # Subscribers/pollers ``` curl -u phil:phil "http://localhost:2586/test103/json?poll=1" {"id":"PhZhyDlKwdhT","sequence_id":"seq15819","time":1768353449,"expires":1773537449,"event":"message","topic":"test103","message":"created seqA=seq15819"} {"id":"5Mxo5LSu2P8N","sequence_id":"seq21256","time":1768353452,"expires":1773537452,"event":"message","topic":"test103","message":"created seqB=seq21256"} {"id":"Wh4u0rSfBmPX","sequence_id":"seq15819","time":1768353455,"expires":1773537455,"event":"message","topic":"test103","message":"updated seqA=seq15819"} {"id":"MLqJJf4xFv18","sequence_id":"seq21256","time":1768353458,"expires":1773537458,"event":"message","topic":"test103","message":"updated seqB=seq21256"} {"id":"cRYDwQYZjOzm","sequence_id":"seq15819","time":1768353461,"expires":1773537461,"event":"message_clear","topic":"test103"} {"id":"0NkR9VR2AGGt","sequence_id":"seq21256","time":1768353464,"expires":1773537464,"event":"message_clear","topic":"test103"} {"id":"qWd8WpK2eTpm","sequence_id":"seq15819","time":1768353467,"expires":1773537467,"event":"message_delete","topic":"test103"} {"id":"M0kEPhZHLV4H","sequence_id":"seq21256","time":1768353470,"expires":1773537470,"event":"message_delete","topic":"test103"} {"id":"BggTMOpN5YHe","sequence_id":"seq21256","time":1768353473,"expires":1773537473,"event":"message","topic":"test103","message":"revived seqB=seq21256"} {"id":"j5FdVMJhIC5A","sequence_id":"seq21256","time":1768353476,"expires":1773537476,"event":"message_delete","topic":"test103"} ``` # Apps Server: - Each message has a sequence ID. If the sequence ID not set when publishing, we set sequenceID = messageID - If sequenceID = messageID, we do NOT output it when polling or after publishing ⭐ - Each update is its own message with a `sequence_id` set - Each clear (=mark read) event is its own message with `event=message_clear` ⭐ - Each deletion event is its own message with `event=message_delete` ⭐ Web app: - When a new message arrives, old messages in the sequence are hard deleted from the local storage - The latest message is stored and displayed. - The existing browser notification is updated. - If the latest message is a `event=message_delete` message, no message is added to the local storage. The browser notification cannot be deleted. - If the latest message is a `event=message_clear` message, the message is marked as read in the local storage. The icon badge number is decreased. The browser popup notification cannot be reliably canceled. ⭐ Android app: - When a new message arrives, old messages in the sequence are soft-deleted from the local storage - The latest message is stored and displayed. - The existing browser notification is updated or cleared - If the latest message is a `event=message_delete` message, no message is added to the local storage. The Android popup notification is canceled (cleared) ⭐ - If the latest message is a `event=message_clear` message, the message is marked as read in the local storage. The icon badge number is decreased.⭐
Author
Owner

@p1gp1g commented on GitHub (Jan 14, 2026):

Once implemented, it shouldn't be hard to send the "sequence id" using a header right? Then it can be possible to comply with web push Topic header:

$ curl -u phil:phil -d "created seqA=$seqA" -H "topic: $seqA" localhost:2586/$topic
$ curl -u phil:phil -d "updated seqA=$seqA" -H "topic: $seqA" localhost:2586/$topic
<!-- gh-comment-id:3748082994 --> @p1gp1g commented on GitHub (Jan 14, 2026): Once implemented, it shouldn't be hard to send the "sequence id" using a header right? Then it can be possible to comply with [web push `Topic` header](https://www.rfc-editor.org/rfc/rfc8030#section-5.4): ```console $ curl -u phil:phil -d "created seqA=$seqA" -H "topic: $seqA" localhost:2586/$topic $ curl -u phil:phil -d "updated seqA=$seqA" -H "topic: $seqA" localhost:2586/$topic
Author
Owner

@binwiederhier commented on GitHub (Jan 14, 2026):

You can pass X-Sequence-ID header though that doesn't comply with the RFC. Adding a Topic: header would be super confusing, sadly, because "topic" is what ntfy uses, so I don't quite know what do to there. How would that be helpful?

<!-- gh-comment-id:3749776760 --> @binwiederhier commented on GitHub (Jan 14, 2026): You can pass `X-Sequence-ID` header though that doesn't comply with the RFC. Adding a `Topic:` header would be super confusing, sadly, because "topic" is what ntfy uses, so I don't quite know what do to there. How would that be helpful?
Author
Owner

@p1gp1g commented on GitHub (Jan 14, 2026):

What about using Topic: header only when ?up GET parameter is given ?

For example, mollysocket, sends push notifications as pings for Molly, a soft-fork of Signal. The push notifications are sent with topic: mollysocket to avoid storing more than one ping on the server.

That way, if a device is offline for an hour and receive like 20 messages, the device only get 1 push notification when back online

<!-- gh-comment-id:3749798091 --> @p1gp1g commented on GitHub (Jan 14, 2026): What about using `Topic:` header only when `?up` GET parameter is given ? For example, mollysocket, sends push notifications as pings for Molly, a soft-fork of Signal. The push notifications are sent with `topic: mollysocket` to avoid storing more than one ping on the server. That way, if a device is offline for an hour and receive like 20 messages, the device only get 1 push notification when back online
Author
Owner

@binwiederhier commented on GitHub (Jan 14, 2026):

I am open to the idea, but we should move this discussion to a different ticket. Also, that would mean that ntfy would have to only forward one notification to UP instead of all intermediate ones. Right now, each message still there (see above).

<!-- gh-comment-id:3750262803 --> @binwiederhier commented on GitHub (Jan 14, 2026): I am open to the idea, but we should move this discussion to a different ticket. Also, that would mean that ntfy would have to only forward one notification to UP instead of all intermediate ones. Right now, each message still there (see above).
Author
Owner

@p1gp1g commented on GitHub (Jan 14, 2026):

I thought the deletion was done on the server. Here it is: https://github.com/binwiederhier/ntfy/issues/1548

<!-- gh-comment-id:3750303462 --> @p1gp1g commented on GitHub (Jan 14, 2026): I thought the deletion was done on the server. Here it is: https://github.com/binwiederhier/ntfy/issues/1548
Author
Owner

@binwiederhier commented on GitHub (Jan 15, 2026):

Here's a question:

Right now, an UPDATED message is still event:message. When we CLEAR or DELETE a message, it's an event:message_clear or event:message_delete. Should an updated message be event:message_update?

So instead of:

{"id":"PhZhyDlKwdhT","event":"message","topic":"test","message":"first message"}
{"id":"Wh4u0rSfBmPX","sequence_id":"PhZhyDlKwdhT","event":"message","topic":"test","message":"Updated seqA=seq15819"}

It would be:

{"id":"PhZhyDlKwdhT","event":"message","topic":"test","message":"first message"}
{"id":"Wh4u0rSfBmPX","sequence_id":"PhZhyDlKwdhT","event":"message_update","topic":"test","message":"Updated seqA=seq15819"}
<!-- gh-comment-id:3756947251 --> @binwiederhier commented on GitHub (Jan 15, 2026): ❓ Here's a question: Right now, an UPDATED message is still `event:message`. When we CLEAR or DELETE a message, it's an `event:message_clear` or `event:message_delete`. Should an updated message be `event:message_update`? So instead of: ``` {"id":"PhZhyDlKwdhT","event":"message","topic":"test","message":"first message"} {"id":"Wh4u0rSfBmPX","sequence_id":"PhZhyDlKwdhT","event":"message","topic":"test","message":"Updated seqA=seq15819"} ``` It would be: ``` {"id":"PhZhyDlKwdhT","event":"message","topic":"test","message":"first message"} {"id":"Wh4u0rSfBmPX","sequence_id":"PhZhyDlKwdhT","event":"message_update","topic":"test","message":"Updated seqA=seq15819"} ```
Author
Owner

@wunter8 commented on GitHub (Jan 15, 2026):

It might be nice for clients to know explicitly which messages are new vs updates (especially if the previous/first message(s) aren't in the cache anymore). And it'd be harder to change this in the future if we don't implement it now. So I'd probably lean toward yes?

<!-- gh-comment-id:3757097071 --> @wunter8 commented on GitHub (Jan 15, 2026): It might be nice for clients to know explicitly which messages are new vs updates (especially if the previous/first message(s) aren't in the cache anymore). And it'd be harder to change this in the future if we don't implement it now. So I'd probably lean toward yes?
Author
Owner

@wunter8 commented on GitHub (Jan 15, 2026):

Although, if the message isn't in the server cache anymore, how will the server know if something is a first message or an update?

Updates will always have a sequence_id, but a first message might have a manually defined sequence_id

<!-- gh-comment-id:3757100791 --> @wunter8 commented on GitHub (Jan 15, 2026): Although, if the message isn't in the server cache anymore, how will the server know if something is a first message or an update? Updates will always have a sequence_id, but a first message might have a manually defined sequence_id
Author
Owner

@binwiederhier commented on GitHub (Jan 15, 2026):

Although, if the message isn't in the server cache anymore, how will the server know if something is a first message or an update?

The message_clear and message_delete events are also in the database. So would this one.

And it'd be harder to change this in the future

This would never change. Backwards compatibility FTW

<!-- gh-comment-id:3757199350 --> @binwiederhier commented on GitHub (Jan 15, 2026): > Although, if the message isn't in the server cache anymore, how will the server know if something is a first message or an update? The `message_clear` and `message_delete` events are also in the database. So would this one. > And it'd be harder to change this in the future This would never change. Backwards compatibility FTW
Author
Owner

@binwiederhier commented on GitHub (Jan 15, 2026):

Updates will always have a sequence_id, but a first message might have a manually defined sequence_id

I didn't realize there is a problem here until I was about to implement this. With this new message_update event, the server now needs to figure out the type of the message, which implies a read from the DB for every publish, or at least every publish to /topic/sequence-id. Not sure if that's great.

Example 1: Normal case, no DB lookup, no ambiguity.

(a) PUT /mytopic   # --> {id:PhZhyDlKwdhT, event:message}
(b) PUT /mytopic/PhZhyDlKwdhT # --> {id:Wh4u0rSfBmPX, sequence_id:PhZhyDlKwdhT, event:message_update}

Example 2.1: Additional DB lookup, ambiguity depending on the message timing. Here: Second message is a message_update.

(a) PUT /mytopic/myseq # --> {id:PhZhyDlKwdhT, sequence_id:myseq, event:message}
    # ^^ --- Requires DB lookup
(b) PUT /mytopic/myseq # --> {id:Wh4u0rSfBmPX, sequence_id:myseq, event:message_update}

Example 2.2: Additional DB lookup, ambiguity depending on the message timing. Here: Second message is a message

(a) PUT /mytopic/myseq # --> {id:PhZhyDlKwdhT, sequence_id:myseq, event:message}
# Wait > 12h, first message PhZhyDlKwdhT is pruned
(b) PUT /mytopic/myseq # --> {id:Wh4u0rSfBmPX, sequence_id:myseq, event:message}
# Event is "message", because first message was deleted ----------------^^^^^^^

I don't like this ambiguity.

<!-- gh-comment-id:3757259348 --> @binwiederhier commented on GitHub (Jan 15, 2026): > Updates will always have a sequence_id, but a first message might have a manually defined sequence_id I didn't realize there is a problem here until I was about to implement this. With this new `message_update` event, the server now needs to figure out the type of the message, which implies a read from the DB for every publish, or at least every publish to /topic/sequence-id. Not sure if that's great. **Example 1:** Normal case, no DB lookup, no ambiguity. ``` (a) PUT /mytopic # --> {id:PhZhyDlKwdhT, event:message} (b) PUT /mytopic/PhZhyDlKwdhT # --> {id:Wh4u0rSfBmPX, sequence_id:PhZhyDlKwdhT, event:message_update} ``` **Example 2.1:** Additional DB lookup, ambiguity depending on the message timing. Here: Second message is a `message_update`. ``` (a) PUT /mytopic/myseq # --> {id:PhZhyDlKwdhT, sequence_id:myseq, event:message} # ^^ --- Requires DB lookup (b) PUT /mytopic/myseq # --> {id:Wh4u0rSfBmPX, sequence_id:myseq, event:message_update} ``` **Example 2.2:** Additional DB lookup, ambiguity depending on the message timing. Here: Second message is a `message` ``` (a) PUT /mytopic/myseq # --> {id:PhZhyDlKwdhT, sequence_id:myseq, event:message} # Wait > 12h, first message PhZhyDlKwdhT is pruned (b) PUT /mytopic/myseq # --> {id:Wh4u0rSfBmPX, sequence_id:myseq, event:message} # Event is "message", because first message was deleted ----------------^^^^^^^ ``` I don't like this ambiguity.
Author
Owner

@wunter8 commented on GitHub (Jan 15, 2026):

Yeah, your example 2.2 is what I meant by "how will the server know if something is a first message or an update?"

<!-- gh-comment-id:3757273681 --> @wunter8 commented on GitHub (Jan 15, 2026): Yeah, your example 2.2 is what I meant by "how will the server know if something is a first message or an update?"
Author
Owner

@binwiederhier commented on GitHub (Jan 15, 2026):

I need to learn how to read :-) I just re-read your comments and it's very clear that you saw this problem before me haha. Soooo.... leave it like it is then?

<!-- gh-comment-id:3757283320 --> @binwiederhier commented on GitHub (Jan 15, 2026): I need to learn how to read :-) I just re-read your comments and it's very clear that you saw this problem before me haha. Soooo.... leave it like it is then?
Author
Owner

@wunter8 commented on GitHub (Jan 15, 2026):

Yeah, probably leave it as is. Both because of the additional DB query on each publish and because of the ambiguity.

I don't see the same issue with message_clear and message_delete because the endpoint will indicate whether it's a clear or delete (without any additional DB query), and the request will always include the relevant sequence_id, so it doesn't matter if the previous message(s) are still in the DB or not

<!-- gh-comment-id:3757303603 --> @wunter8 commented on GitHub (Jan 15, 2026): Yeah, probably leave it as is. Both because of the additional DB query on each publish and because of the ambiguity. I don't see the same issue with `message_clear` and `message_delete` because the endpoint will indicate whether it's a clear or delete (without any additional DB query), and the request will always include the relevant sequence_id, so it doesn't matter if the previous message(s) are still in the DB or not
Author
Owner

@binwiederhier commented on GitHub (Jan 19, 2026):

This is done and merged. It'll be in the next release: https://docs.ntfy.sh/publish/#updating-deleting-notifications

I am testing it right now, and will likely cut a testing Android release either today or tomorrow.

<!-- gh-comment-id:3766064507 --> @binwiederhier commented on GitHub (Jan 19, 2026): This is done and merged. It'll be in the next release: https://docs.ntfy.sh/publish/#updating-deleting-notifications I am testing it right now, and will likely cut a testing Android release either today or tomorrow.
Author
Owner

@wcypierre commented on GitHub (Jan 19, 2026):

Any love for iOS clients?

<!-- gh-comment-id:3766129531 --> @wcypierre commented on GitHub (Jan 19, 2026): Any love for iOS clients?
Author
Owner

@binwiederhier commented on GitHub (Jan 19, 2026):

Any love for iOS clients?

No love from me, though there has been a user on Discord that made a new ntfy iOS app. They seem competent. If that doesn't work out, I'll likely eventually pick that up myself again.

<!-- gh-comment-id:3766140122 --> @binwiederhier commented on GitHub (Jan 19, 2026): > Any love for iOS clients? No love from me, though there has been a user on Discord that made a new ntfy iOS app. They seem competent. If that doesn't work out, I'll likely eventually pick that up myself again.
Author
Owner

@arminus commented on GitHub (Jan 22, 2026):

Highly appreciate this! 🥇 Works like a charm with the web client, is there an updated android version already somewhere?

<!-- gh-comment-id:3783987529 --> @arminus commented on GitHub (Jan 22, 2026): Highly appreciate this! 🥇 Works like a charm with the web client, is there an updated android version already somewhere?
Author
Owner

@binwiederhier commented on GitHub (Jan 22, 2026):

The Android version is rolling out on Play over the next week, or you can sign up for betas there and get it now.
.

<!-- gh-comment-id:3784081006 --> @binwiederhier commented on GitHub (Jan 22, 2026): The Android version is rolling out on Play over the next week, or you can sign up for betas there and get it now. .
Author
Owner

@shale-aspect commented on GitHub (Jan 25, 2026):

Is it possible to add event to the supported fields of Publish as json? I see that sequence_id is supported (and updating messages works nicely), but my attempts to clear or delete messages haven't been successful so far.

<!-- gh-comment-id:3797188113 --> @shale-aspect commented on GitHub (Jan 25, 2026): Is it possible to add `event` to the supported fields of [Publish as json](https://docs.ntfy.sh/publish/?h=delete#publish-as-json)? I see that `sequence_id` is supported (and updating messages works nicely), but my attempts to clear or delete messages haven't been successful so far.
Author
Owner

@wunter8 commented on GitHub (Jan 25, 2026):

@shale-aspect I think it'd be best to open a new issue for that. Something like "allow clearing and deleting a message via "publish as JSON""

<!-- gh-comment-id:3797199882 --> @wunter8 commented on GitHub (Jan 25, 2026): @shale-aspect I think it'd be best to open a new issue for that. Something like "allow clearing and deleting a message via "publish as JSON""
Author
Owner

@shale-aspect commented on GitHub (Jan 25, 2026):

@wunter8 Thanks for the heads-up, I opened #1573.

<!-- gh-comment-id:3797240021 --> @shale-aspect commented on GitHub (Jan 25, 2026): @wunter8 Thanks for the heads-up, I opened #1573.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
starred/ntfy#237
No description provided.