<!DOCTYPE html> <html lang="en"> <head> <title>ntfy.sh</title> <style> body { font-size: 1.2em; line-height: 130%; } #error { color: darkred; font-style: italic; } #main { max-width: 900px; margin: 0 auto 50px auto; } </style> </head> <body> <div id="main"> <h1>ntfy.sh - simple HTTP-based pub-sub</h1> <p> <b>ntfy</b> (pronounce: <i>notify</i>) is a simple <b>HTTP-based pub-sub notification service and tool</b>. It allows you to send <b>desktop notifications via scripts</b>, entirely <b>without signup or cost</b>. It's also <a href="https://github.com/binwiederhier/ntfy">open source</a> if you want to run your own. </p> <p id="error"></p> <h2>Subscribe to a topic</h2> <p> Topics are created on the fly by subscribing to them. You can create and subscribe to a topic either in this web UI, or in your own app by subscribing to an <a href="https://developer.mozilla.org/en-US/docs/Web/API/EventSource">EventSource</a>, a JSON feed, or raw feed. </p> <p> Because there is no sign-up, <b>the topic is essentially a password</b>, so pick something that's not easily guessable. </p> <h3>Subscribe via web</h3> <p> If you subscribe to a topic via this web UI in the field below, messages published to any subscribed topic will show up as <b>desktop notification</b>. </p> <form id="subscribeForm"> <p> <label for="topicField">Topic ID:</label> <input type="text" id="topicField" placeholder="Letters, numbers, _ and -" pattern="[-_A-Za-z]{1,64}" autofocus /> <input type="submit" id="subscribeButton" value="Subscribe topic" /> </p> </form> <p id="topicsHeader">Subscribed topics:</p> <ul id="topicsList"></ul> <h3>Subscribe via your app, or via the CLI</h3> <tt> curl -s ntfy.sh/mytopic/raw # one message per line (\n are replaced with a space)<br/> curl -s ntfy.sh/mytopic/json # one JSON message per line<br/> curl -s ntfy.sh/mytopic/sse # server-sent events (SSE) stream </tt> <h3>Publishing messages</h3> <p> Publishing messages can be done via PUT or POST using. Here's an example using <tt>curl</tt>: </p> <tt> curl -d "long process is done" ntfy.sh/mytopic </tt> <p> Messages published to a non-existing topic or a topic without subscribers will not be delivered later. There is (currently) no buffering of any kind. If you're not listening, the message won't be delivered. </p> </div> <script type="text/javascript"> let topics = {}; const topicsHeader = document.getElementById("topicsHeader"); const topicsList = document.getElementById("topicsList"); const topicField = document.getElementById("topicField"); const subscribeButton = document.getElementById("subscribeButton"); const subscribeForm = document.getElementById("subscribeForm"); const errorField = document.getElementById("error"); const subscribe = (topic) => { if (Notification.permission !== "granted") { Notification.requestPermission().then((permission) => { if (permission === "granted") { subscribeInternal(topic, 0); } else { showNotificationDeniedError(); } }); } else { subscribeInternal(topic, 0); } }; const subscribeInternal = (topic, delaySec) => { setTimeout(() => { // Render list entry let topicEntry = document.getElementById(`topic-${topic}`); if (!topicEntry) { topicEntry = document.createElement('li'); topicEntry.id = `topic-${topic}`; topicEntry.innerHTML = `${topic} <button onclick="unsubscribe('${topic}')">Unsubscribe</button>`; topicsList.appendChild(topicEntry); } topicsHeader.style.display = ''; // Open event source let eventSource = new EventSource(`${topic}/sse`); eventSource.onopen = () => { topicEntry.innerHTML = `${topic} <button onclick="unsubscribe('${topic}')">Unsubscribe</button>`; delaySec = 0; // Reset on successful connection }; eventSource.onerror = (e) => { const newDelaySec = (delaySec + 5 <= 30) ? delaySec + 5 : 30; topicEntry.innerHTML = `${topic} <i>(Reconnecting in ${newDelaySec}s ...)</i> <button onclick="unsubscribe('${topic}')">Unsubscribe</button>`; eventSource.close() subscribeInternal(topic, newDelaySec); }; eventSource.onmessage = (e) => { const event = JSON.parse(e.data); new Notification(event.message); }; topics[topic] = eventSource; localStorage.setItem('topics', JSON.stringify(Object.keys(topics))); }, delaySec * 1000); }; const unsubscribe = (topic) => { topics[topic].close(); delete topics[topic]; localStorage.setItem('topics', JSON.stringify(Object.keys(topics))); document.getElementById(`topic-${topic}`).remove(); if (Object.keys(topics).length === 0) { topicsHeader.style.display = 'none'; } }; const showError = (msg) => { errorField.innerHTML = msg; topicField.disabled = true; subscribeButton.disabled = true; }; const showBrowserIncompatibleError = () => { showError("Your browser is not compatible to use the web-based desktop notifications."); }; const showNotificationDeniedError = () => { showError("You have blocked desktop notifications for this website. Please unblock them and refresh to use the web-based desktop notifications."); }; subscribeForm.onsubmit = function () { if (!topicField.value) { return false; } subscribe(topicField.value); topicField.value = ""; return false; }; // Disable Web UI if notifications of EventSource are not available if (!window["Notification"] || !window["EventSource"]) { showBrowserIncompatibleError(); } else if (Notification.permission === "denied") { showNotificationDeniedError(); } // Reset UI topicField.value = ""; // Restore topics const storedTopics = localStorage.getItem('topics'); if (storedTopics && Notification.permission === "granted") { const storedTopicsArray = JSON.parse(storedTopics) storedTopicsArray.forEach((topic) => { subscribeInternal(topic, 0); }); if (storedTopicsArray.length === 0) { topicsHeader.style.display = 'none'; } } else { topicsHeader.style.display = 'none'; } </script> </body> </html>