Skip to content

Publishing

Publishing messages can be done via HTTP PUT/POST or via the ntfy CLI. Topics are created on the fly by subscribing or publishing to them. Because there is no sign-up, the topic is essentially a password, so pick something that's not easily guessable.

Here's an example showing how to publish a simple message using a POST request:

curl -d "Backup successful 😀" ntfy.sh/mytopic
ntfy publish mytopic "Backup successful 😀"
POST /mytopic HTTP/1.1
Host: ntfy.sh

Backup successful 😀
fetch('https://ntfy.sh/mytopic', {
  method: 'POST', // PUT works too
  body: 'Backup successful 😀'
})
http.Post("https://ntfy.sh/mytopic", "text/plain",
    strings.NewReader("Backup successful 😀"))
$Request = @{
  Method = "POST"
  URI = "https://ntfy.sh/mytopic"
  Body = "Backup successful"
}
Invoke-RestMethod @Request
requests.post("https://ntfy.sh/mytopic", 
    data="Backup successful 😀".encode(encoding='utf-8'))
file_get_contents('https://ntfy.sh/mytopic', false, stream_context_create([
    'http' => [
        'method' => 'POST', // PUT also works
        'header' => 'Content-Type: text/plain',
        'content' => 'Backup successful 😀'
    ]
]));

If you have the Android app installed on your phone, this will create a notification that looks like this:

basic notification

Android notification

There are more features related to publishing messages: You can set a notification priority, a title, and tag messages 🥳 🎉. Here's an example that uses some of them at together:

curl \
  -H "Title: Unauthorized access detected" \
  -H "Priority: urgent" \
  -H "Tags: warning,skull" \
  -d "Remote access to phils-laptop detected. Act right away." \
  ntfy.sh/phil_alerts
ntfy publish \
    --title "Unauthorized access detected" \
    --tags warning,skull \
    --priority urgent \
    mytopic \
    "Remote access to phils-laptop detected. Act right away."
POST /phil_alerts HTTP/1.1
Host: ntfy.sh
Title: Unauthorized access detected
Priority: urgent
Tags: warning,skull

Remote access to phils-laptop detected. Act right away.
fetch('https://ntfy.sh/phil_alerts', {
    method: 'POST', // PUT works too
    body: 'Remote access to phils-laptop detected. Act right away.',
    headers: {
        'Title': 'Unauthorized access detected',
        'Priority': 'urgent',
        'Tags': 'warning,skull'
    }
})
req, _ := http.NewRequest("POST", "https://ntfy.sh/phil_alerts",
    strings.NewReader("Remote access to phils-laptop detected. Act right away."))
req.Header.Set("Title", "Unauthorized access detected")
req.Header.Set("Priority", "urgent")
req.Header.Set("Tags", "warning,skull")
http.DefaultClient.Do(req)
$Request = @{
  Method = "POST"
  URI = "https://ntfy.sh/phil_alerts"
  Headers = @{
    Title = "Unauthorized access detected"
    Priority = "urgent"
    Tags = "warning,skull"
  }
  Body = "Remote access to phils-laptop detected. Act right away."
}
Invoke-RestMethod @Request
requests.post("https://ntfy.sh/phil_alerts",
    data="Remote access to phils-laptop detected. Act right away.",
    headers={
        "Title": "Unauthorized access detected",
        "Priority": "urgent",
        "Tags": "warning,skull"
    })
file_get_contents('https://ntfy.sh/phil_alerts', false, stream_context_create([
    'http' => [
        'method' => 'POST', // PUT also works
        'header' =>
            "Content-Type: text/plain\r\n" .
            "Title: Unauthorized access detected\r\n" .
            "Priority: urgent\r\n" .
            "Tags: warning,skull",
        'content' => 'Remote access to phils-laptop detected. Act right away.'
    ]
]));

priority notification

Urgent notification with tags and title

You can also do multi-line messages. Here's an example using a click action, an action button, an external image attachment and email publishing:

curl \
  -H "Click: https://home.nest.com/" \
  -H "Attach: https://nest.com/view/yAxkasd.jpg" \
  -H "Actions: http, Open door, https://api.nest.com/open/yAxkasd, clear=true" \
  -H "Email: phil@example.com" \
  -d "There's someone at the door. 🐶

Please check if it's a good boy or a hooman. 
Doggies have been known to ring the doorbell." \
  ntfy.sh/mydoorbell
ntfy publish \
    --click="https://home.nest.com/" \
    --attach="https://nest.com/view/yAxkasd.jpg" \
    --actions="http, Open door, https://api.nest.com/open/yAxkasd, clear=true" \
    --email="phil@example.com" \
    mydoorbell \
    "There's someone at the door. 🐶

Please check if it's a good boy or a hooman. 
Doggies have been known to ring the doorbell."
POST /mydoorbell HTTP/1.1
Host: ntfy.sh
Click: https://home.nest.com/
Attach: https://nest.com/view/yAxkasd.jpg
Actions: http, Open door, https://api.nest.com/open/yAxkasd, clear=true
Email: phil@example.com

There's someone at the door. 🐶

Please check if it's a good boy or a hooman. 
Doggies have been known to ring the doorbell.
fetch('https://ntfy.sh/mydoorbell', {
    method: 'POST', // PUT works too
    headers: {
        'Click': 'https://home.nest.com/',
        'Attach': 'https://nest.com/view/yAxkasd.jpg',
        'Actions': 'http, Open door, https://api.nest.com/open/yAxkasd, clear=true',
        'Email': 'phil@example.com'
    },
    body: `There's someone at the door. 🐶

Please check if it's a good boy or a hooman. 
Doggies have been known to ring the doorbell.`,
})
req, _ := http.NewRequest("POST", "https://ntfy.sh/mydoorbell",
    strings.NewReader(`There's someone at the door. 🐶

Please check if it's a good boy or a hooman. 
Doggies have been known to ring the doorbell.`))
req.Header.Set("Click", "https://home.nest.com/")
req.Header.Set("Attach", "https://nest.com/view/yAxkasd.jpg")
req.Header.Set("Actions", "http, Open door, https://api.nest.com/open/yAxkasd, clear=true")
req.Header.Set("Email", "phil@example.com")
http.DefaultClient.Do(req)
$Request = @{
  Method = "POST"
  URI = "https://ntfy.sh/mydoorbell"
  Headers = @{
    Click = "https://home.nest.com"
    Attach = "https://nest.com/view/yAxksd.jpg"
    Actions = "http, Open door, https://api.nest.com/open/yAxkasd, clear=true"
    Email = "phil@example.com"
  }
  Body = "There's someone at the door. 🐶`n
  `n
  Please check if it's a good boy or a hooman.`n
  Doggies have been known to ring the doorbell.`n"
}
Invoke-RestMethod @Request
requests.post("https://ntfy.sh/mydoorbell",
    data="""There's someone at the door. 🐶

Please check if it's a good boy or a hooman.
Doggies have been known to ring the doorbell.""".encode('utf-8'),
    headers={
        "Click": "https://home.nest.com/",
        "Attach": "https://nest.com/view/yAxkasd.jpg",
        "Actions": "http, Open door, https://api.nest.com/open/yAxkasd, clear=true",
        "Email": "phil@example.com"
    })
file_get_contents('https://ntfy.sh/mydoorbell', false, stream_context_create([
    'http' => [
        'method' => 'POST', // PUT also works
        'header' =>
            "Content-Type: text/plain\r\n" .
            "Click: https://home.nest.com/\r\n" .
            "Attach: https://nest.com/view/yAxkasd.jpg\r\n" .
            "Actions": "http, Open door, https://api.nest.com/open/yAxkasd, clear=true\r\n" .
            "Email": "phil@example.com\r\n",
        'content' => 'There\'s someone at the door. 🐶

Please check if it\'s a good boy or a hooman.
Doggies have been known to ring the doorbell.'
    ]
]));

priority notification

Notification using a click action, a user action, with an external image attachment and forwarded via email

Message title

Supported on:

The notification title is typically set to the topic short URL (e.g. ntfy.sh/mytopic). To override the title, you can set the X-Title header (or any of its aliases: Title, ti, or t).

curl -H "X-Title: Dogs are better than cats" -d "Oh my ..." ntfy.sh/controversial
curl -H "Title: Dogs are better than cats" -d "Oh my ..." ntfy.sh/controversial
curl -H "t: Dogs are better than cats" -d "Oh my ..." ntfy.sh/controversial
ntfy publish \
    -t "Dogs are better than cats" \
    controversial "Oh my ..."
POST /controversial HTTP/1.1
Host: ntfy.sh
Title: Dogs are better than cats

Oh my ...
fetch('https://ntfy.sh/controversial', {
    method: 'POST',
    body: 'Oh my ...',
    headers: { 'Title': 'Dogs are better than cats' }
})
req, _ := http.NewRequest("POST", "https://ntfy.sh/controversial", strings.NewReader("Oh my ..."))
req.Header.Set("Title", "Dogs are better than cats")
http.DefaultClient.Do(req)
$Request = @{
  Method = "POST"
  URI = "https://ntfy.sh/controversial"
  Headers = @{
    Title = "Dogs are better than cats"
  }
  Body = "Oh my ..."
}
Invoke-RestMethod @Request
requests.post("https://ntfy.sh/controversial",
    data="Oh my ...",
    headers={ "Title": "Dogs are better than cats" })
file_get_contents('https://ntfy.sh/controversial', false, stream_context_create([
    'http' => [
        'method' => 'POST',
        'header' =>
            "Content-Type: text/plain\r\n" .
            "Title: Dogs are better than cats",
        'content' => 'Oh my ...'
    ]
]));

notification with title

Detail view of notification with title

Info

ntfy supports UTF-8 in HTTP headers, but not every library or programming language does. If non-ASCII characters are causing issues for you in the title (i.e. you're seeing ? symbols), you may also encode any header (including the title) as RFC 2047, e.g. =?UTF-8?B?8J+HqfCfh6o=?= (base64), or =?UTF-8?Q?=C3=84pfel?= (quoted-printable).

Message priority

Supported on:

All messages have a priority, which defines how urgently your phone notifies you. On Android, you can set custom notification sounds and vibration patterns on your phone to map to these priorities (see Android config).

The following priorities exist:

Priority Icon ID Name Description
Max priority min priority 5 max/urgent Really long vibration bursts, default notification sound with a pop-over notification.
High priority min priority 4 high Long vibration burst, default notification sound with a pop-over notification.
Default priority (none) 3 default Short default vibration and sound. Default notification behavior.
Low priority min priority 2 low No vibration or sound. Notification will not visibly show up until notification drawer is pulled down.
Min priority min priority 1 min No vibration or sound. The notification will be under the fold in "Other notifications".

You can set the priority with the header X-Priority (or any of its aliases: Priority, prio, or p).

curl -H "X-Priority: 5" -d "An urgent message" ntfy.sh/phil_alerts
curl -H "Priority: low" -d "Low priority message" ntfy.sh/phil_alerts
curl -H p:4 -d "A high priority message" ntfy.sh/phil_alerts
ntfy publish \ 
    -p 5 \
    phil_alerts An urgent message
POST /phil_alerts HTTP/1.1
Host: ntfy.sh
Priority: 5

An urgent message
fetch('https://ntfy.sh/phil_alerts', {
    method: 'POST',
    body: 'An urgent message',
    headers: { 'Priority': '5' }
})
req, _ := http.NewRequest("POST", "https://ntfy.sh/phil_alerts", strings.NewReader("An urgent message"))
req.Header.Set("Priority", "5")
http.DefaultClient.Do(req)
$Request = @{
  Method = 'POST'
  URI = "https://ntfy.sh/phil_alerts"
  Headers = @{
    Priority = "5"
  }
  Body = "An urgent message"
}
Invoke-RestMethod @Request
requests.post("https://ntfy.sh/phil_alerts",
    data="An urgent message",
    headers={ "Priority": "5" })
file_get_contents('https://ntfy.sh/phil_alerts', false, stream_context_create([
    'http' => [
        'method' => 'POST',
        'header' =>
            "Content-Type: text/plain\r\n" .
            "Priority: 5",
        'content' => 'An urgent message'
    ]
]));

priority notification

Detail view of priority notifications

Tags & emojis 🥳 🎉

Supported on:

You can tag messages with emojis and other relevant strings:

  • Emojis: If a tag matches an emoji short code, it'll be converted to an emoji and prepended to title or message.
  • Other tags: If a tag doesn't match, it will be listed below the notification.

This feature is useful for things like warnings (⚠️, ️🚨, or 🚩), but also to simply tag messages otherwise (e.g. script names, hostnames, etc.). Use the emoji short code list to figure out what tags can be converted to emojis. Here's an excerpt of emojis I've found very useful in alert messages:

TagEmoji
+1👍
partying_face🥳
tada🎉
heavy_check_mark✔️
loudspeaker📢
......
TagEmoji
-1👎️
warning⚠️
rotating_light️🚨
triangular_flag_on_post🚩
skull💀
......
TagEmoji
facepalm🤦
no_entry
no_entry_sign🚫
cd💿
computer💻
......

You can set tags with the X-Tags header (or any of its aliases: Tags, tag, or ta). Specify multiple tags by separating them with a comma, e.g. tag1,tag2,tag3.

curl -H "X-Tags: warning,mailsrv13,daily-backup" -d "Backup of mailsrv13 failed" ntfy.sh/backups
curl -H "Tags: horse,unicorn" -d "Unicorns are just horses with unique horns" ntfy.sh/backups
curl -H ta:dog -d "Dogs are awesome" ntfy.sh/backups
ntfy publish \
    --tags=warning,mailsrv13,daily-backup \
    backups "Backup of mailsrv13 failed"
POST /backups HTTP/1.1
Host: ntfy.sh
Tags: warning,mailsrv13,daily-backup

Backup of mailsrv13 failed
fetch('https://ntfy.sh/backups', {
    method: 'POST',
    body: 'Backup of mailsrv13 failed',
    headers: { 'Tags': 'warning,mailsrv13,daily-backup' }
})
req, _ := http.NewRequest("POST", "https://ntfy.sh/backups", strings.NewReader("Backup of mailsrv13 failed"))
req.Header.Set("Tags", "warning,mailsrv13,daily-backup")
http.DefaultClient.Do(req)
$Request = @{
  Method = "POST"
  URI = "https://ntfy.sh/backups"
  Headers = @{
    Tags = "warning,mailsrv13,daily-backup"
  }
  Body = "Backup of mailsrv13 failed"
}
Invoke-RestMethod @Request
requests.post("https://ntfy.sh/backups",
    data="Backup of mailsrv13 failed",
    headers={ "Tags": "warning,mailsrv13,daily-backup" })
file_get_contents('https://ntfy.sh/backups', false, stream_context_create([
    'http' => [
        'method' => 'POST',
        'header' =>
            "Content-Type: text/plain\r\n" .
            "Tags: warning,mailsrv13,daily-backup",
        'content' => 'Backup of mailsrv13 failed'
    ]
]));

priority notification

Detail view of notifications with tags

Info

ntfy supports UTF-8 in HTTP headers, but not every library or programming language does. If non-ASCII characters are causing issues for you in the title (i.e. you're seeing ? symbols), you may also encode the tags header or individual tags as RFC 2047, e.g. tag1,=?UTF-8?B?8J+HqfCfh6o=?= (base64), or =?UTF-8?Q?=C3=84pfel?=,tag2 (quoted-printable).

Markdown formatting

Supported on:

You can format messages using Markdown 🤩. That means you can use bold text, italicized text, links, images, and more. Supported Markdown features (web app only for now):

By default, messages sent to ntfy are rendered as plain text. To enable Markdown, set the X-Markdown header (or any of its aliases: Markdown, or md) to true (or 1 or yes), or set the Content-Type header to text/markdown. As of today, Markdown is only supported in the web app. Here's an example of how to enable Markdown formatting:

curl \
    -d "Look ma, **bold text**, *italics*, ..." \
    -H "Markdown: yes" \
    ntfy.sh/mytopic
ntfy publish \
    --markdown \
    mytopic \
    "Look ma, **bold text**, *italics*, ..."
POST /mytopic HTTP/1.1
Host: ntfy.sh
Markdown: yes

Look ma, **bold text**, *italics*, ...
fetch('https://ntfy.sh/mytopic', {
  method: 'POST', // PUT works too
  body: 'Look ma, **bold text**, *italics*, ...',
  headers: { 'Markdown': 'yes' }
})
http.Post("https://ntfy.sh/mytopic", "text/markdown",
    strings.NewReader("Look ma, **bold text**, *italics*, ..."))

// or
req, _ := http.NewRequest("POST", "https://ntfy.sh/mytopic", 
    strings.NewReader("Look ma, **bold text**, *italics*, ..."))
req.Header.Set("Markdown", "yes")
http.DefaultClient.Do(req)
$Request = @{
  Method = "POST"
  URI = "https://ntfy.sh/mytopic"
  Body = "Look ma, **bold text**, *italics*, ..."
  Headers = @{
    Markdown = "yes"
  }
}
Invoke-RestMethod @Request
requests.post("https://ntfy.sh/mytopic", 
    data="Look ma, **bold text**, *italics*, ..."
    headers={ "Markdown": "yes" }))
file_get_contents('https://ntfy.sh/mytopic', false, stream_context_create([
    'http' => [
        'method' => 'POST', // PUT also works
        'header' => 'Content-Type: text/markdown', // !
        'content' => 'Look ma, **bold text**, *italics*, ...'
    ]
]));

Here's what that looks like in the web app:

markdown

Markdown formatting in the web app

Scheduled delivery

Supported on:

You can delay the delivery of messages and let ntfy send them at a later date. This can be used to send yourself reminders or even to execute commands at a later date (if your subscriber acts on messages).

Usage is pretty straight forward. You can set the delivery time using the X-Delay header (or any of its aliases: Delay, X-At, At, X-In or In), either by specifying a Unix timestamp (e.g. 1639194738), a duration (e.g. 30m, 3h, 2 days), or a natural language time string (e.g. 10am, 8:30pm, tomorrow, 3pm, Tuesday, 7am, and more).

As of today, the minimum delay you can set is 10 seconds and the maximum delay is 3 days. This can be configured with the message-delay-limit option).

For the purposes of message caching, scheduled messages are kept in the cache until 12 hours after they were delivered (or whatever the server-side cache duration is set to). For instance, if a message is scheduled to be delivered in 3 days, it'll remain in the cache for 3 days and 12 hours. Also note that naturally, turning off server-side caching is not possible in combination with this feature.

curl -H "At: tomorrow, 10am" -d "Good morning" ntfy.sh/hello
curl -H "In: 30min" -d "It's 30 minutes later now" ntfy.sh/reminder
curl -H "Delay: 1639194738" -d "Unix timestamps are awesome" ntfy.sh/itsaunixsystem
ntfy publish \
    --at="tomorrow, 10am" \
    hello "Good morning"
POST /hello HTTP/1.1
Host: ntfy.sh
At: tomorrow, 10am

Good morning
fetch('https://ntfy.sh/hello', {
    method: 'POST',
    body: 'Good morning',
    headers: { 'At': 'tomorrow, 10am' }
})
req, _ := http.NewRequest("POST", "https://ntfy.sh/hello", strings.NewReader("Good morning"))
req.Header.Set("At", "tomorrow, 10am")
http.DefaultClient.Do(req)
$Request = @{
  Method = "POST"
  URI = "https://ntfy.sh/hello"
  Headers = @{
    At = "tomorrow, 10am"
  }
  Body = "Good morning"
}
Invoke-RestMethod @Request
requests.post("https://ntfy.sh/hello",
    data="Good morning",
    headers={ "At": "tomorrow, 10am" })
file_get_contents('https://ntfy.sh/backups', false, stream_context_create([
    'http' => [
        'method' => 'POST',
        'header' =>
            "Content-Type: text/plain\r\n" .
            "At: tomorrow, 10am",
        'content' => 'Good morning'
    ]
]));

Here are a few examples (assuming today's date is 12/10/2021, 9am, Eastern Time Zone):

Delay/At/In headerMessage will be delivered atExplanation
30m12/10/2021, 9:30am30 minutes from now
2 hours12/10/2021, 11:30am2 hours from now
1 day12/11/2021, 9am24 hours from now
10am12/10/2021, 10amToday at 10am (same day, because it's only 9am)
8am12/11/2021, 8amTomorrow at 8am (because it's 9am already)
163915200012/10/2021, 11am (EST) Today at 11am (EST)

Webhooks (publish via GET)

Supported on:

In addition to using PUT/POST, you can also send to topics via simple HTTP GET requests. This makes it easy to use a ntfy topic as a webhook, or if your client has limited HTTP support (e.g. like the MacroDroid Android app).

To send messages via HTTP GET, simply call the /publish endpoint (or its aliases /send and /trigger). Without any arguments, this will send the message triggered to the topic. However, you can provide all arguments that are also supported as HTTP headers as URL-encoded arguments. Be sure to check the list of all supported parameters and headers for details.

For instance, assuming your topic is mywebhook, you can simply call /mywebhook/trigger to send a message (aka trigger the webhook):

curl ntfy.sh/mywebhook/trigger
ntfy trigger mywebhook
GET /mywebhook/trigger HTTP/1.1
Host: ntfy.sh
fetch('https://ntfy.sh/mywebhook/trigger')
http.Get("https://ntfy.sh/mywebhook/trigger")
Invoke-RestMethod "ntfy.sh/mywebhook/trigger"
requests.get("https://ntfy.sh/mywebhook/trigger")
file_get_contents('https://ntfy.sh/mywebhook/trigger');

To add a custom message, simply append the message= URL parameter. And of course you can set the message priority, the message title, and tags as well. For a full list of possible parameters, check the list of supported parameters and headers.

Here's an example with a custom message, tags and a priority:

curl "ntfy.sh/mywebhook/publish?message=Webhook+triggered&priority=high&tags=warning,skull"
ntfy publish \
    -p 5 --tags=warning,skull \
    mywebhook "Webhook triggered"
GET /mywebhook/publish?message=Webhook+triggered&priority=high&tags=warning,skull HTTP/1.1
Host: ntfy.sh
fetch('https://ntfy.sh/mywebhook/publish?message=Webhook+triggered&priority=high&tags=warning,skull')
http.Get("https://ntfy.sh/mywebhook/publish?message=Webhook+triggered&priority=high&tags=warning,skull")
Invoke-RestMethod "ntfy.sh/mywebhook/publish?message=Webhook+triggered&priority=high&tags=warning,skull"
requests.get("https://ntfy.sh/mywebhook/publish?message=Webhook+triggered&priority=high&tags=warning,skull")
file_get_contents('https://ntfy.sh/mywebhook/publish?message=Webhook+triggered&priority=high&tags=warning,skull');

Message templating

Supported on:

Templating lets you format a JSON message body into human-friendly message and title text using Go templates (see tutorials here, here, and here). This is specifically useful when combined with webhooks from services such as GitHub, Grafana, or other services that emit JSON webhooks.

Instead of using a separate bridge program to parse the webhook body into the format ntfy expects, you can include a templated message and/or a templated title which will be populated based on the fields of the webhook body (so long as the webhook body is valid JSON).

You can enable templating by setting the X-Template header (or its aliases Template or tpl) to yes or 1, or (more appropriately for webhooks) by setting the ?template=yes query parameter. Then, include templates in your message and/or title, using the following stanzas (see Go docs for detailed syntax):

  • Variables,, e.g. {{.alert.title}} or An error occurred: {{.error.desc}}
  • Conditionals (if/else, e.g. {{if eq .action "opened"}}..{{else}}..{{end}}, see example)
  • Loops (e.g. {{range .errors}}..{{end}}, see example)

A good way to experiment with Go templates is the Go Template Playground. It is highly recommended to test your templates there first (example for Grafana alert).

Info

Please note that the Go templating language is quite terrible. My apologies for using it for this feature. It is the best option for Go-based programs like ntfy. Stay calm and don't harm yourself or others in despair. You can do it. I believe in you!

Here's an example for a Grafana alert:

notification with actions

Grafana webhook, formatted using templates

This was sent using the following templates and payloads

{{range .alerts}}
  {{.annotations.summary}}

  Values:
  {{range $k,$v := .values}}
    - {{$k}}={{$v}}
  {{end}}
{{end}}
{{.title}}
# Additional URL encoding (see https://www.urlencoder.org/) is necessary for Grafana, 
# and may be required for other tools too

https://ntfy.sh/mytopic?tpl=1&t=%7B%7B.title%7D%7D&m=%7B%7Brange%20.alerts%7D%7D%7B%7B.annotations.summary%7D%7D%5Cn%5CnValues%3A%5Cn%7B%7Brange%20%24k%2C%24v%20%3A%3D%20.values%7D%7D-%20%7B%7B%24k%7D%7D%3D%7B%7B%24v%7D%7D%5Cn%7B%7Bend%7D%7D%7B%7Bend%7D%7D
{"receiver":"ntfy\\.example\\.com/alerts","status":"resolved","alerts":[{"status":"resolved","labels":{"alertname":"Load avg 15m too high","grafana_folder":"Node alerts","instance":"10.108.0.2:9100","job":"node-exporter"},"annotations":{"summary":"15m load average too high"},"startsAt":"2024-03-15T02:28:00Z","endsAt":"2024-03-15T02:42:00Z","generatorURL":"localhost:3000/alerting/grafana/NW9oDw-4z/view","fingerprint":"becbfb94bd81ef48","silenceURL":"localhost:3000/alerting/silence/new?alertmanager=grafana&matcher=alertname%3DLoad+avg+15m+too+high&matcher=grafana_folder%3DNode+alerts&matcher=instance%3D10.108.0.2%3A9100&matcher=job%3Dnode-exporter","dashboardURL":"","panelURL":"","values":{"B":18.98211314475876,"C":0},"valueString":"[ var='B' labels={__name__=node_load15, instance=10.108.0.2:9100, job=node-exporter} value=18.98211314475876 ], [ var='C' labels={__name__=node_load15, instance=10.108.0.2:9100, job=node-exporter} value=0 ]"}],"groupLabels":{"alertname":"Load avg 15m too high","grafana_folder":"Node alerts"},"commonLabels":{"alertname":"Load avg 15m too high","grafana_folder":"Node alerts","instance":"10.108.0.2:9100","job":"node-exporter"},"commonAnnotations":{"summary":"15m load average too high"},"externalURL":"localhost:3000/","version":"1","groupKey":"{}:{alertname=\"Load avg 15m too high\", grafana_folder=\"Node alerts\"}","truncatedAlerts":0,"orgId":1,"title":"[RESOLVED] Load avg 15m too high Node alerts (10.108.0.2:9100 node-exporter)","state":"ok","message":"**Resolved**\n\nValue: B=18.98211314475876, C=0\nLabels:\n - alertname = Load avg 15m too high\n - grafana_folder = Node alerts\n - instance = 10.108.0.2:9100\n - job = node-exporter\nAnnotations:\n - summary = 15m load average too high\nSource: localhost:3000/alerting/grafana/NW9oDw-4z/view\nSilence: localhost:3000/alerting/silence/new?alertmanager=grafana&matcher=alertname%3DLoad+avg+15m+too+high&matcher=grafana_folder%3DNode+alerts&matcher=instance%3D10.108.0.2%3A9100&matcher=job%3Dnode-exporter\n"}

Here's an easier example with a shorter JSON payload:

# To use { and } in the URL without encoding, we need to turn of
# curl's globbing using --globoff

curl \
    --globoff \
    -d '{"hostname": "phil-pc", "error": {"level": "severe", "desc": "Disk has run out of space"}}' \
    'ntfy.sh/mytopic?tpl=yes&t={{.hostname}}:+A+{{.error.level}}+error+has+occurred&m=Error+message:+{{.error.desc}}'
POST /mytopic?tpl=yes&t={{.hostname}}:+A+{{.error.level}}+error+has+occurred&m=Error+message:+{{.error.desc}} HTTP/1.1
Host: ntfy.sh

{"hostname": "phil-pc", "error": {"level": "severe", "desc": "Disk has run out of space"}}
fetch('https://ntfy.sh/mytopic?tpl=yes&t={{.hostname}}:+A+{{.error.level}}+error+has+occurred&m=Error+message:+{{.error.desc}}', {
    method: 'POST',
    body: '{"hostname": "phil-pc", "error": {"level": "severe", "desc": "Disk has run out of space"}}'
})
body := `{"hostname": "phil-pc", "error": {"level": "severe", "desc": "Disk has run out of space"}}`
uri := "https://ntfy.sh/mytopic?tpl=yes&t={{.hostname}}:+A+{{.error.level}}+error+has+occurred&m=Error+message:+{{.error.desc}}"
req, _ := http.NewRequest("POST", uri, strings.NewReader(body))
http.DefaultClient.Do(req)
$Request = @{
    Method = "POST"
    URI = "https://ntfy.sh/mytopic?tpl=yes&t={{.hostname}}:+A+{{.error.level}}+error+has+occurred&m=Error+message:+{{.error.desc}}"
    Body = '{"hostname": "phil-pc", "error": {"level": "severe", "desc": "Disk has run out of space"}}'
    ContentType = "application/json"
}
Invoke-RestMethod @Request
requests.post(
    "https://ntfy.sh/mytopic?tpl=yes&t={{.hostname}}:+A+{{.error.level}}+error+has+occurred&m=Error+message:+{{.error.desc}}",
    data='{"hostname": "phil-pc", "error": {"level": "severe", "desc": "Disk has run out of space"}}'
)
file_get_contents("https://ntfy.sh/mytopic?tpl=yes&t={{.hostname}}:+A+{{.error.level}}+error+has+occurred&m=Error+message:+{{.error.desc}}", false, stream_context_create([
    'http' => [
        'method' => 'POST',
        'header' => "Content-Type: application/json",
        'content' => '{"hostname": "phil-pc", "error": {"level": "severe", "desc": "Disk has run out of space"}}'
    ]
]));

This example uses the message/m and title/t query parameters, but obviously this also works with the corresponding Message/Title headers. It will send a notification with a title phil-pc: A severe error has occurred and a message Error message: Disk has run out of space.

Publish as JSON

Supported on:

For some integrations with other tools (e.g. Jellyfin, overseerr), adding custom headers to HTTP requests may be tricky or impossible, so ntfy also allows publishing the entire message as JSON in the request body.

To publish as JSON, simple PUT/POST the JSON object directly to the ntfy root URL. The message format is described below the example.

Info

To publish as JSON, you must PUT/POST to the ntfy root URL, not to the topic URL. Be sure to check that you're POST-ing to https://ntfy.sh/ (correct), and not to https://ntfy.sh/mytopic (incorrect).

Here's an example using most supported parameters. Check the table below for a complete list. The topic parameter is the only required one:

curl ntfy.sh \
  -d '{
    "topic": "mytopic",
    "message": "Disk space is low at 5.1 GB",
    "title": "Low disk space alert",
    "tags": ["warning","cd"],
    "priority": 4,
    "attach": "https://filesrv.lan/space.jpg",
    "filename": "diskspace.jpg",
    "click": "https://homecamera.lan/xasds1h2xsSsa/",
    "actions": [{ "action": "view", "label": "Admin panel", "url": "https://filesrv.lan/admin" }]
  }'
POST / HTTP/1.1
Host: ntfy.sh

{
    "topic": "mytopic",
    "message": "Disk space is low at 5.1 GB",
    "title": "Low disk space alert",
    "tags": ["warning","cd"],
    "priority": 4,
    "attach": "https://filesrv.lan/space.jpg",
    "filename": "diskspace.jpg",
    "click": "https://homecamera.lan/xasds1h2xsSsa/",
    "actions": [{ "action": "view", "label": "Admin panel", "url": "https://filesrv.lan/admin" }]
}
fetch('https://ntfy.sh', {
    method: 'POST',
    body: JSON.stringify({
        "topic": "mytopic",
        "message": "Disk space is low at 5.1 GB",
        "title": "Low disk space alert",
        "tags": ["warning","cd"],
        "priority": 4,
        "attach": "https://filesrv.lan/space.jpg",
        "filename": "diskspace.jpg",
        "click": "https://homecamera.lan/xasds1h2xsSsa/",
        "actions": [{ "action": "view", "label": "Admin panel", "url": "https://filesrv.lan/admin" }]
    })
})
// You should probably use json.Marshal() instead and make a proper struct,
// or even just use req.Header.Set() like in the other examples, but for the 
// sake of the example, this is easier.

body := `{
    "topic": "mytopic",
    "message": "Disk space is low at 5.1 GB",
    "title": "Low disk space alert",
    "tags": ["warning","cd"],
    "priority": 4,
    "attach": "https://filesrv.lan/space.jpg",
    "filename": "diskspace.jpg",
    "click": "https://homecamera.lan/xasds1h2xsSsa/",
    "actions": [{ "action": "view", "label": "Admin panel", "url": "https://filesrv.lan/admin" }]
}`
req, _ := http.NewRequest("POST", "https://ntfy.sh/", strings.NewReader(body))
http.DefaultClient.Do(req)
$Request = @{
  Method = "POST"
  URI = "https://ntfy.sh"
  Body = ConvertTo-JSON @{
    Topic    = "mytopic"
    Title    = "Low disk space alert"
    Message  = "Disk space is low at 5.1 GB"
    Priority = 4
    Attach   = "https://filesrv.lan/space.jpg"
    FileName = "diskspace.jpg"
    Tags     = @("warning", "cd")
    Click    = "https://homecamera.lan/xasds1h2xsSsa/"
    Actions  = @(
      @{ 
        Action = "view"
        Label  = "Admin panel"
        URL    = "https://filesrv.lan/admin"
      }
    )
  }
  ContentType = "application/json"
}
Invoke-RestMethod @Request
requests.post("https://ntfy.sh/",
    data=json.dumps({
        "topic": "mytopic",
        "message": "Disk space is low at 5.1 GB",
        "title": "Low disk space alert",
        "tags": ["warning","cd"],
        "priority": 4,
        "attach": "https://filesrv.lan/space.jpg",
        "filename": "diskspace.jpg",
        "click": "https://homecamera.lan/xasds1h2xsSsa/",
        "actions": [{ "action": "view", "label": "Admin panel", "url": "https://filesrv.lan/admin" }]
    })
)
file_get_contents('https://ntfy.sh/', false, stream_context_create([
    'http' => [
        'method' => 'POST',
        'header' => "Content-Type: application/json",
        'content' => json_encode([
            "topic": "mytopic",
            "message": "Disk space is low at 5.1 GB",
            "title": "Low disk space alert",
            "tags": ["warning","cd"],
            "priority": 4,
            "attach": "https://filesrv.lan/space.jpg",
            "filename": "diskspace.jpg",
            "click": "https://homecamera.lan/xasds1h2xsSsa/",
            "actions": [["action": "view", "label": "Admin panel", "url": "https://filesrv.lan/admin" ]]
        ])
    ]
]));

The JSON message format closely mirrors the format of the message you can consume when you subscribe via the API (see JSON message format for details), but is not exactly identical. Here's an overview of all the supported fields:

Field Required Type Example Description
topic ✔️ string topic1 Target topic name
message - string Some message Message body; set to triggered if empty or not passed
title - string Some title Message title
tags - string array ["tag1","tag2"] List of tags that may or not map to emojis
priority - int (one of: 1, 2, 3, 4, or 5) 4 Message priority with 1=min, 3=default and 5=max
actions - JSON array (see action buttons) Custom user action buttons for notifications
click - URL https://example.com Website opened when notification is clicked
attach - URL https://example.com/file.jpg URL of an attachment, see attach via URL
markdown - bool true Set to true if the message is Markdown-formatted
icon - string https://example.com/icon.png URL to use as notification icon
filename - string file.jpg File name of the attachment
delay - string 30min, 9am Timestamp or duration for delayed delivery
email - e-mail address phil@example.com E-mail address for e-mail notifications
call - phone number or 'yes' +1222334444 or yes Phone number to use for voice call

Action buttons

Supported on:

You can add action buttons to notifications to allow yourself to react to a notification directly. This is incredibly useful and has countless applications.

You can control your home appliances (open/close garage door, change temperature on thermostat, ...), react to common monitoring alerts (clear logs when disk is full, ...), and many other things. The sky is the limit.

As of today, the following actions are supported:

  • view: Opens a website or app when the action button is tapped
  • broadcast: Sends an Android broadcast intent when the action button is tapped (only supported on Android)
  • http: Sends HTTP POST/GET/PUT request when the action button is tapped

Here's an example of what a notification with actions can look like:

notification with actions

Notification with two user actions

Defining actions

You can define up to three user actions in your notifications, using either of the following methods:

Using a header

To define actions using the X-Actions header (or any of its aliases: Actions, Action), use the following format:

action=<action1>, label=<label1>, paramN=... [; action=<action2>, label=<label2>, ...]
<action1>, <label1>, paramN=... [; <action2>, <label2>, ...]

Multiple actions are separated by a semicolon (;), and key/value pairs are separated by commas (,). Values may be quoted with double quotes (") or single quotes (') if the value itself contains commas or semicolons.

The action= and label= prefix are optional in all actions, and the url= prefix is optional in the view and http action. The only limitation of this format is that depending on your language/library, UTF-8 characters may not work. If they don't, use the JSON array format instead.

As an example, here's how you can create the above notification using this format. Refer to the view action and http action section for details on the specific actions:

body='{"temperature": 65}'
curl \
    -d "You left the house. Turn down the A/C?" \
    -H "Actions: view, Open portal, https://home.nest.com/, clear=true; \
                 http, Turn down, https://api.nest.com/, body='$body'" \
    ntfy.sh/myhome
body='{"temperature": 65}'
ntfy publish \
    --actions="view, Open portal, https://home.nest.com/, clear=true; \
               http, Turn down, https://api.nest.com/, body='$body'" \
    myhome \
    "You left the house. Turn down the A/C?"
POST /myhome HTTP/1.1
Host: ntfy.sh
Actions: view, Open portal, https://home.nest.com/, clear=true; http, Turn down, https://api.nest.com/, body='{"temperature": 65}'

You left the house. Turn down the A/C?
fetch('https://ntfy.sh/myhome', {
    method: 'POST',
    body: 'You left the house. Turn down the A/C?',
    headers: { 
        'Actions': 'view, Open portal, https://home.nest.com/, clear=true; http, Turn down, https://api.nest.com/, body=\'{"temperature": 65}\'' 
    }
})
req, _ := http.NewRequest("POST", "https://ntfy.sh/myhome", strings.NewReader("You left the house. Turn down the A/C?"))
req.Header.Set("Actions", "view, Open portal, https://home.nest.com/, clear=true; http, Turn down, https://api.nest.com/, body='{\"temperature\": 65}'")
http.DefaultClient.Do(req)
$Request = @{
  Method = "POST"
  URI = "https://ntfy.sh/myhome"
  Headers = @{
    Actions="view, Open portal, https://home.nest.com/, clear=true; http, Turn down, https://api.nest.com/, body='{\"temperature\": 65}'"
  }
  Body = "You left the house. Turn down the A/C?"
}
Invoke-RestMethod @Request
requests.post("https://ntfy.sh/myhome",
    data="You left the house. Turn down the A/C?",
    headers={ "Actions": "view, Open portal, https://home.nest.com/, clear=true; http, Turn down, https://api.nest.com/, body='{\"temperature\": 65}'" })
file_get_contents('https://ntfy.sh/reddit_alerts', false, stream_context_create([
    'http' => [
        'method' => 'POST',
        'header' =>
            "Content-Type: text/plain\r\n" .
            "Actions: view, Open portal, https://home.nest.com/, clear=true; http, Turn down, https://api.nest.com/, body='{\"temperature\": 65}'",
        'content' => 'You left the house. Turn down the A/C?'
    ]
]));

Info

ntfy supports UTF-8 in HTTP headers, but not every library or programming language does. If non-ASCII characters are causing issues for you in the title (i.e. you're seeing ? symbols), you may also encode any header (including actions) as RFC 2047, e.g. =?UTF-8?B?8J+HqfCfh6o=?= (base64), or =?UTF-8?Q?=C3=84pfel?= (quoted-printable).

Using a JSON array

Alternatively, the same actions can be defined as JSON array, if the notification is defined as part of the JSON body (see publish as JSON):

curl ntfy.sh \
  -d '{
    "topic": "myhome",
    "message": "You left the house. Turn down the A/C?",
    "actions": [
      {
        "action": "view",
        "label": "Open portal",
        "url": "https://home.nest.com/",
        "clear": true
      },
      {
        "action": "http",
        "label": "Turn down",
        "url": "https://api.nest.com/",
        "body": "{\"temperature\": 65}"
      }
    ]
  }'
ntfy publish \
    --actions '[
        {
            "action": "view",
            "label": "Open portal",
            "url": "https://home.nest.com/",
            "clear": true
        },
        {
            "action": "http",
            "label": "Turn down",
            "url": "https://api.nest.com/",
            "body": "{\"temperature\": 65}"
        }
    ]' \
    myhome \
    "You left the house. Turn down the A/C?"
POST / HTTP/1.1
Host: ntfy.sh

{
    "topic": "myhome",
    "message": "You left the house. Turn down the A/C?",
    "actions": [
      {
        "action": "view",
        "label": "Open portal",
        "url": "https://home.nest.com/",
        "clear": true
      },
      {
        "action": "http",
        "label": "Turn down",
        "url": "https://api.nest.com/",
        "body": "{\"temperature\": 65}"
      }
    ]
}
fetch('https://ntfy.sh', {
    method: 'POST',
    body: JSON.stringify({
        topic: "myhome",
        message: "You left the house. Turn down the A/C?",
        actions: [
            {
                action: "view",
                label: "Open portal",
                url: "https://home.nest.com/",
                clear: true
            },
            {
                action: "http",
                label: "Turn down",
                url: "https://api.nest.com/",
                body: "{\"temperature\": 65}"
            }
        ]
    })
})
// You should probably use json.Marshal() instead and make a proper struct,
// but for the sake of the example, this is easier.

body := `{
    "topic": "myhome",
    "message": "You left the house. Turn down the A/C?",
    "actions": [
      {
        "action": "view",
        "label": "Open portal",
        "url": "https://home.nest.com/",
        "clear": true
      },
      {
        "action": "http",
        "label": "Turn down",
        "url": "https://api.nest.com/",
        "body": "{\"temperature\": 65}"
      }
    ]
}`
req, _ := http.NewRequest("POST", "https://ntfy.sh/", strings.NewReader(body))
http.DefaultClient.Do(req)
$Request = @{
  Method = "POST"
  URI = "https://ntfy.sh"
  Body = ConvertTo-JSON @{
    Topic   = "myhome"
    Message = "You left the house. Turn down the A/C?"
    Actions = @(
      @{
        Action = "view"
        Label  = "Open portal"
        URL    = "https://home.nest.com/"
        Clear  = $true
      },
      @{
        Action = "http"
        Label  = "Turn down"
        URL    = "https://api.nest.com/"
        Body   = '{"temperature": 65}'
      }
    )
  }
  ContentType = "application/json"
}
Invoke-RestMethod @Request
requests.post("https://ntfy.sh/",
    data=json.dumps({
        "topic": "myhome",
        "message": "You left the house. Turn down the A/C?",
        "actions": [
            {
                "action": "view",
                "label": "Open portal",
                "url": "https://home.nest.com/",
                "clear": true
            },
            {
                "action": "http",
                "label": "Turn down",
                "url": "https://api.nest.com/",
                "body": "{\"temperature\": 65}"
            }
        ]
    })
)
file_get_contents('https://ntfy.sh/', false, stream_context_create([
    'http' => [
        'method' => 'POST',
        'header' => "Content-Type: application/json",
        'content' => json_encode([
            "topic": "myhome",
            "message": "You left the house. Turn down the A/C?",
            "actions": [
                [
                    "action": "view",
                    "label": "Open portal",
                    "url": "https://home.nest.com/",
                    "clear": true
                ],
                [
                    "action": "http",
                    "label": "Turn down",
                    "url": "https://api.nest.com/",
                    "headers": [
                        "Authorization": "Bearer ..."
                    ],
                    "body": "{\"temperature\": 65}"
                ]
            ]
        ])
    ]
]));

The required/optional fields for each action depend on the type of the action itself. Please refer to view action, broadcast action, and http action for details.

Open website/app

Supported on:

The view action opens a website or app when the action button is tapped, e.g. a browser, a Google Maps location, or even a deep link into Twitter or a show ntfy topic. How exactly the action is handled depends on how Android and your desktop browser treat the links. Normally it'll just open a link in the browser.

Examples:

  • http:// or https:// will open your browser (or an app if it registered for a URL)
  • mailto: links will open your mail app, e.g. mailto:phil@example.com
  • geo: links will open Google Maps, e.g. geo:0,0?q=1600+Amphitheatre+Parkway,+Mountain+View,+CA
  • ntfy:// links will open ntfy (see ntfy:// links), e.g. ntfy://ntfy.sh/stats
  • twitter:// links will open Twitter, e.g. twitter://user?screen_name=..
  • ...

Here's an example using the X-Actions header:

curl \
    -d "Somebody retweeted your tweet." \
    -H "Actions: view, Open Twitter, https://twitter.com/binwiederhier/status/1467633927951163392" \
ntfy.sh/myhome
ntfy publish \
    --actions="view, Open Twitter, https://twitter.com/binwiederhier/status/1467633927951163392" \
    myhome \
    "Somebody retweeted your tweet."
POST /myhome HTTP/1.1
Host: ntfy.sh
Actions: view, Open Twitter, https://twitter.com/binwiederhier/status/1467633927951163392

Somebody retweeted your tweet.
fetch('https://ntfy.sh/myhome', {
    method: 'POST',
    body: 'Somebody retweeted your tweet.',
    headers: { 
        'Actions': 'view, Open Twitter, https://twitter.com/binwiederhier/status/1467633927951163392' 
    }
})
req, _ := http.NewRequest("POST", "https://ntfy.sh/myhome", strings.NewReader("Somebody retweeted your tweet."))
req.Header.Set("Actions", "view, Open Twitter, https://twitter.com/binwiederhier/status/1467633927951163392")
http.DefaultClient.Do(req)
$Request = @{
  Method = "POST"
  URI = "https://ntfy.sh/myhome"
  Headers = @{
    Actions = "view, Open Twitter, https://twitter.com/binwiederhier/status/1467633927951163392"
  }
  Body = "Somebody retweeted your tweet."
}
Invoke-RestMethod @Request
requests.post("https://ntfy.sh/myhome",
    data="Somebody retweeted your tweet.",
    headers={ "Actions": "view, Open Twitter, https://twitter.com/binwiederhier/status/1467633927951163392" })
file_get_contents('https://ntfy.sh/reddit_alerts', false, stream_context_create([
    'http' => [
        'method' => 'POST',
        'header' =>
            "Content-Type: text/plain\r\n" .
            "Actions: view, Open Twitter, https://twitter.com/binwiederhier/status/1467633927951163392",
        'content' => 'Somebody retweeted your tweet.'
    ]
]));

And the same example using JSON publishing:

curl ntfy.sh \
  -d '{
    "topic": "myhome",
    "message": "Somebody retweeted your tweet.",
    "actions": [
      {
        "action": "view",
        "label": "Open Twitter",
        "url": "https://twitter.com/binwiederhier/status/1467633927951163392"
      }
    ]
  }'
ntfy publish \
    --actions '[
        {
            "action": "view",
            "label": "Open Twitter",
            "url": "https://twitter.com/binwiederhier/status/1467633927951163392"
        }
    ]' \
    myhome \
    "Somebody retweeted your tweet."
POST / HTTP/1.1
Host: ntfy.sh

{
    "topic": "myhome",
    "message": "Somebody retweeted your tweet.",
    "actions": [
      {
        "action": "view",
        "label": "Open Twitter",
        "url": "https://twitter.com/binwiederhier/status/1467633927951163392"
      }
    ]
}
fetch('https://ntfy.sh', {
    method: 'POST',
    body: JSON.stringify({
        topic: "myhome",
        message": "Somebody retweeted your tweet.",
        actions: [
            {
                action: "view",
                label: "Open Twitter",
                url: "https://twitter.com/binwiederhier/status/1467633927951163392"
            }
        ]
    })
})
// You should probably use json.Marshal() instead and make a proper struct,
// but for the sake of the example, this is easier.

body := `{
    "topic": "myhome",
    "message": "Somebody retweeted your tweet.",
    "actions": [
      {
        "action": "view",
        "label": "Open Twitter",
        "url": "https://twitter.com/binwiederhier/status/1467633927951163392"
      }
    ]
}`
req, _ := http.NewRequest("POST", "https://ntfy.sh/", strings.NewReader(body))
http.DefaultClient.Do(req)
$Request = @{
  Method = "POST"
  URI = "https://ntfy.sh"
  Body = ConvertTo-JSON @{
    Topic = "myhome"
    Message = "Somebody retweeted your tweet."
    Actions = @(
      @{
        Action = "view"
        Label  = "Open Twitter"
        URL    = "https://twitter.com/binwiederhier/status/1467633927951163392"
      }
    )
  }
  ContentType = "application/json"
}
Invoke-RestMethod @Request
requests.post("https://ntfy.sh/",
    data=json.dumps({
        "topic": "myhome",
        "message": "Somebody retweeted your tweet.",
        "actions": [
            {
                "action": "view",
                "label": "Open Twitter",
                "url": "https://twitter.com/binwiederhier/status/1467633927951163392"
            }
        ]
    })
)
file_get_contents('https://ntfy.sh/', false, stream_context_create([
    'http' => [
        'method' => 'POST',
        'header' => "Content-Type: application/json",
        'content' => json_encode([
            "topic": "myhome",
            "message": "Somebody retweeted your tweet.",
            "actions": [
                [
                    "action": "view",
                    "label": "Open Twitter",
                    "url": "https://twitter.com/binwiederhier/status/1467633927951163392"
                ]
            ]
        ])
    ]
]));

The view action supports the following fields:

Field Required Type Default Example Description
action ✔️ string - view Action type (must be view)
label ✔️ string - Turn on light Label of the action button in the notification
url ✔️ URL - https://example.com URL to open when action is tapped
clear -️ boolean false true Clear notification after action button is tapped

Send Android broadcast

Supported on:

The broadcast action sends an Android broadcast intent when the action button is tapped. This allows integration into automation apps such as MacroDroid or Tasker, which basically means you can do everything your phone is capable of. Examples include taking pictures, launching/killing apps, change device settings, write/read files, etc.

By default, the intent action io.heckel.ntfy.USER_ACTION is broadcast, though this can be changed with the intent parameter (see below). To send extras, use the extras parameter. Currently, only string extras are supported.

Info

If you have no idea what this is, check out the automation apps section, which shows how to integrate Tasker and MacroDroid with screenshots. The action button integration is identical, except that you have to use the intent action io.heckel.ntfy.USER_ACTION instead.

Here's an example using the X-Actions header:

curl \
    -d "Your wife requested you send a picture of yourself." \
    -H "Actions: broadcast, Take picture, extras.cmd=pic, extras.camera=front" \
ntfy.sh/wifey
ntfy publish \
    --actions="broadcast, Take picture, extras.cmd=pic, extras.camera=front" \
    wifey \
    "Your wife requested you send a picture of yourself."
POST /wifey HTTP/1.1
Host: ntfy.sh
Actions: broadcast, Take picture, extras.cmd=pic, extras.camera=front

Your wife requested you send a picture of yourself.
fetch('https://ntfy.sh/wifey', {
    method: 'POST',
    body: 'Your wife requested you send a picture of yourself.',
    headers: { 
        'Actions': 'broadcast, Take picture, extras.cmd=pic, extras.camera=front' 
    }
})
req, _ := http.NewRequest("POST", "https://ntfy.sh/wifey", strings.NewReader("Your wife requested you send a picture of yourself."))
req.Header.Set("Actions", "broadcast, Take picture, extras.cmd=pic, extras.camera=front")
http.DefaultClient.Do(req)
$Request = @{
  Method = "POST"
  URI = "https://ntfy.sh/wifey"
  Headers = @{
    Actions = "broadcast, Take picture, extras.cmd=pic, extras.camera=front"
  }
  Body = "Your wife requested you send a picture of yourself."
}
Invoke-RestMethod @Request
requests.post("https://ntfy.sh/wifey",
    data="Your wife requested you send a picture of yourself.",
    headers={ "Actions": "broadcast, Take picture, extras.cmd=pic, extras.camera=front" })
file_get_contents('https://ntfy.sh/wifey', false, stream_context_create([
    'http' => [
        'method' => 'POST',
        'header' =>
            "Content-Type: text/plain\r\n" .
            "Actions: broadcast, Take picture, extras.cmd=pic, extras.camera=front",
        'content' => 'Your wife requested you send a picture of yourself.'
    ]
]));

And the same example using JSON publishing:

curl ntfy.sh \
  -d '{
    "topic": "wifey",
    "message": "Your wife requested you send a picture of yourself.",
    "actions": [
      {
        "action": "broadcast",
        "label": "Take picture",
        "extras": {
            "cmd": "pic",
            "camera": "front"
        }
      }
    ]
  }'
ntfy publish \
    --actions '[
        {
            "action": "broadcast",
            "label": "Take picture",
            "extras": {
                "cmd": "pic",
                "camera": "front"
            }
        }
    ]' \
    wifey \
    "Your wife requested you send a picture of yourself."
POST / HTTP/1.1
Host: ntfy.sh

{
    "topic": "wifey",
    "message": "Your wife requested you send a picture of yourself.",
    "actions": [
      {
        "action": "broadcast",
        "label": "Take picture",
        "extras": {
            "cmd": "pic",
            "camera": "front"
        }
      }
    ]
}
fetch('https://ntfy.sh', {
    method: 'POST',
    body: JSON.stringify({
        topic: "wifey",
        message": "Your wife requested you send a picture of yourself.",
        actions: [
            {
                "action": "broadcast",
                "label": "Take picture",
                "extras": {
                    "cmd": "pic",
                    "camera": "front"
                }
            }
        ]
    })
})
// You should probably use json.Marshal() instead and make a proper struct,
// but for the sake of the example, this is easier.

body := `{
    "topic": "wifey",
    "message": "Your wife requested you send a picture of yourself.",
    "actions": [
      {
        "action": "broadcast",
        "label": "Take picture",
        "extras": {
            "cmd": "pic",
            "camera": "front"
        }
      }
    ]
}`
req, _ := http.NewRequest("POST", "https://ntfy.sh/", strings.NewReader(body))
http.DefaultClient.Do(req)
# Powershell requires the 'Depth' argument to equal 3 here to expand 'Extras',
# otherwise it will read System.Collections.Hashtable in the returned JSON
$Request = @{
  Method = "POST"
  URI = "https://ntfy.sh"
  Body = ConvertTo-Json -Depth 3 @{
    Topic = "wifey"
    Message = "Your wife requested you send a picture of yourself."
    Actions = @(
      @{
        Action = "broadcast"
        Label = "Take picture"
        Extras = @{
          CMD ="pic"
          Camera = "front"
        }
      }
    )
  }
  ContentType = "application/json"
}
Invoke-RestMethod @Request
requests.post("https://ntfy.sh/",
    data=json.dumps({
        "topic": "wifey",
        "message": "Your wife requested you send a picture of yourself.",
        "actions": [
            {
                "action": "broadcast",
                "label": "Take picture",
                "extras": {
                    "cmd": "pic",
                    "camera": "front"
                }
            }
        ]
    })
)
file_get_contents('https://ntfy.sh/', false, stream_context_create([
    'http' => [
        'method' => 'POST',
        'header' => "Content-Type: application/json",
        'content' => json_encode([
            "topic": "wifey",
            "message": "Your wife requested you send a picture of yourself.",
            "actions": [
                [
                "action": "broadcast",
                "label": "Take picture",
                "extras": [
                    "cmd": "pic",
                    "camera": "front"
                ]
            ]
        ])
    ]
]));

The broadcast action supports the following fields:

Field Required Type Default Example Description
action ✔️ string - broadcast Action type (must be broadcast)
label ✔️ string - Turn on light Label of the action button in the notification
intent -️ string io.heckel.ntfy.USER_ACTION com.example.AN_INTENT Android intent name, default is io.heckel.ntfy.USER_ACTION
extras -️ map of strings - see above Android intent extras. Currently, only string extras are supported. When publishing as JSON, extras are passed as a map. When the simple format is used, use extras.<param>=<value>.
clear -️ boolean false true Clear notification after action button is tapped

Send HTTP request

Supported on:

The http action sends a HTTP request when the action button is tapped. You can use this to trigger REST APIs for whatever systems you have, e.g. opening the garage door, or turning on/off lights.

By default, this action sends a POST request (not GET!), though this can be changed with the method parameter. The only required parameter is url. Headers can be passed along using the headers parameter.

Here's an example using the X-Actions header:

curl \
    -d "Garage door has been open for 15 minutes. Close it?" \
    -H "Actions: http, Close door, https://api.mygarage.lan/, method=PUT, headers.Authorization=Bearer zAzsx1sk.., body={\"action\": \"close\"}" \
    ntfy.sh/myhome
ntfy publish \
    --actions="http, Close door, https://api.mygarage.lan/, method=PUT, headers.Authorization=Bearer zAzsx1sk.., body={\"action\": \"close\"}" \
    myhome \
    "Garage door has been open for 15 minutes. Close it?"
POST /myhome HTTP/1.1
Host: ntfy.sh
Actions: http, Close door, https://api.mygarage.lan/, method=PUT, headers.Authorization=Bearer zAzsx1sk.., body={"action": "close"}

Garage door has been open for 15 minutes. Close it?
fetch('https://ntfy.sh/myhome', {
    method: 'POST',
    body: 'Garage door has been open for 15 minutes. Close it?',
    headers: { 
        'Actions': 'http, Close door, https://api.mygarage.lan/, method=PUT, headers.Authorization=Bearer zAzsx1sk.., body={\"action\": \"close\"}' 
    }
})
req, _ := http.NewRequest("POST", "https://ntfy.sh/myhome", strings.NewReader("Garage door has been open for 15 minutes. Close it?"))
req.Header.Set("Actions", "http, Close door, https://api.mygarage.lan/, method=PUT, headers.Authorization=Bearer zAzsx1sk.., body={\"action\": \"close\"}")
http.DefaultClient.Do(req)
$Request = @{
  Method = "POST"
  URI = "https://ntfy.sh/myhome"
  Headers = @{
    Actions="http, Close door, https://api.mygarage.lan/, method=PUT, headers.Authorization=Bearer zAzsx1sk.., body={\"action\": \"close\"}"
  }
  Body = "Garage door has been open for 15 minutes. Close it?"
}
Invoke-RestMethod @Request
requests.post("https://ntfy.sh/myhome",
    data="Garage door has been open for 15 minutes. Close it?",
    headers={ "Actions": "http, Close door, https://api.mygarage.lan/, method=PUT, headers.Authorization=Bearer zAzsx1sk.., body={\"action\": \"close\"}" })
file_get_contents('https://ntfy.sh/reddit_alerts', false, stream_context_create([
    'http' => [
        'method' => 'POST',
        'header' =>
            "Content-Type: text/plain\r\n" .
            'Actions: http, Close door, https://api.mygarage.lan/, method=PUT, headers.Authorization=Bearer zAzsx1sk.., body={\"action\": \"close\"}',
        'content' => 'Garage door has been open for 15 minutes. Close it?'
    ]
]));

And the same example using JSON publishing:

curl ntfy.sh \
  -d '{
    "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\"}"
      }
    ]
  }'
ntfy publish \
    --actions '[
        {
          "action": "http",
          "label": "Close door",
          "url": "https://api.mygarage.lan/",
          "method": "PUT",
          "headers": {
            "Authorization": "Bearer zAzsx1sk.."
          },
          "body": "{\"action\": \"close\"}"
        }
    ]' \
    myhome \
    "Garage door has been open for 15 minutes. Close it?"
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\"}"
      }
    ]
}
fetch('https://ntfy.sh', {
    method: 'POST',
    body: JSON.stringify({
        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\"}"
          }
        ]
    })
})
// You should probably use json.Marshal() instead and make a proper struct,
// but for the sake of the example, this is easier.

body := `{
    "topic": "myhome",
    "message": "Garage door has been open for 15 minutes. Close it?",
    "actions": [
      {
        "action": "http",
        "label": "Close door",
        "method": "PUT",
        "url": "https://api.mygarage.lan/",
        "headers": {
          "Authorization": "Bearer zAzsx1sk.."
        },
        "body": "{\"action\": \"close\"}"
      }
    ]
}`
req, _ := http.NewRequest("POST", "https://ntfy.sh/", strings.NewReader(body))
http.DefaultClient.Do(req)
# Powershell requires the 'Depth' argument to equal 3 here to expand 'headers', 
# otherwise it will read System.Collections.Hashtable in the returned JSON

$Request = @{
  Method = "POST"
  URI = "https://ntfy.sh"
  Body = ConvertTo-Json -Depth 3 @{
    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    = ConvertTo-JSON @{Action = "close"}
      }
    )
  }
  ContentType = "application/json"
}
Invoke-RestMethod @Request
requests.post("https://ntfy.sh/",
    data=json.dumps({
        "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\"}"
            }
        ]
    })
)
file_get_contents('https://ntfy.sh/', false, stream_context_create([
    'http' => [
        'method' => 'POST',
        'header' => "Content-Type: application/json",
        'content' => json_encode([
            "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 http action supports the following fields:

Field Required Type Default Example Description
action ✔️ string - http Action type (must be http)
label ✔️ string - Open garage door Label of the action button in the notification
url ✔️ string - https://ntfy.sh/mytopic URL to which the HTTP request will be sent
method -️ GET/POST/PUT/... POST ⚠️ GET HTTP method to use for request, default is POST ⚠️
headers -️ map of strings - see above HTTP headers to pass in request. When publishing as JSON, headers are passed as a map. When the simple format is used, use headers.<header1>=<value>.
body -️ string empty some body, somebody? HTTP body
clear -️ boolean false true Clear notification after HTTP request succeeds. If the request fails, the notification is not cleared.

Click action

Supported on:

You can define which URL to open when a notification is clicked. This may be useful if your notification is related to a Zabbix alert or a transaction that you'd like to provide the deep-link for. Tapping the notification will open the web browser (or the app) and open the website.

To define a click action for the notification, pass a URL as the value of the X-Click header (or its alias Click). If you pass a website URL (http:// or https://) the web browser will open. If you pass another URI that can be handled by another app, the responsible app may open.

Examples:

  • http:// or https:// will open your browser (or an app if it registered for a URL)
  • mailto: links will open your mail app, e.g. mailto:phil@example.com
  • geo: links will open Google Maps, e.g. geo:0,0?q=1600+Amphitheatre+Parkway,+Mountain+View,+CA
  • ntfy:// links will open ntfy (see ntfy:// links), e.g. ntfy://ntfy.sh/stats
  • twitter:// links will open Twitter, e.g. twitter://user?screen_name=..
  • ...

Here's an example that will open Reddit when the notification is clicked:

curl \
    -d "New messages on Reddit" \
    -H "Click: https://www.reddit.com/message/messages" \
    ntfy.sh/reddit_alerts
ntfy publish \
    --click="https://www.reddit.com/message/messages" \
    reddit_alerts "New messages on Reddit"
POST /reddit_alerts HTTP/1.1
Host: ntfy.sh
Click: https://www.reddit.com/message/messages 

New messages on Reddit
fetch('https://ntfy.sh/reddit_alerts', {
    method: 'POST',
    body: 'New messages on Reddit',
    headers: { 'Click': 'https://www.reddit.com/message/messages' }
})
req, _ := http.NewRequest("POST", "https://ntfy.sh/reddit_alerts", strings.NewReader("New messages on Reddit"))
req.Header.Set("Click", "https://www.reddit.com/message/messages")
http.DefaultClient.Do(req)
$Request = @{
  Method = "POST"
  URI = "https://ntfy.sh/reddit_alerts"
  Headers = @{ Click="https://www.reddit.com/message/messages" }
  Body = "New messages on Reddit"
}
Invoke-RestMethod @Request
requests.post("https://ntfy.sh/reddit_alerts",
    data="New messages on Reddit",
    headers={ "Click": "https://www.reddit.com/message/messages" })
file_get_contents('https://ntfy.sh/reddit_alerts', false, stream_context_create([
    'http' => [
        'method' => 'POST',
        'header' =>
            "Content-Type: text/plain\r\n" .
            "Click: https://www.reddit.com/message/messages",
        'content' => 'New messages on Reddit'
    ]
]));

Attachments

Supported on:

You can send images and other files to your phone as attachments to a notification. The attachments are then downloaded onto your phone (depending on size and setting automatically), and can be used from the Downloads folder.

There are two different ways to send attachments:

Attach local file

To send a file from your computer as an attachment, you can send it as the PUT request body. If a message is greater than the maximum message size (4,096 bytes) or consists of non UTF-8 characters, the ntfy server will automatically detect the mime type and size, and send the message as an attachment file. To send smaller text-only messages or files as attachments, you must pass a filename by passing the X-Filename header or query parameter (or any of its aliases Filename, File or f).

By default, and how ntfy.sh is configured, the max attachment size is 15 MB (with 100 MB total per visitor). Attachments expire after 3 hours, which typically is plenty of time for the user to download it, or for the Android app to auto-download it. Please also check out the other limits below.

Here's an example showing how to upload an image:

curl \
    -T flower.jpg \
    -H "Filename: flower.jpg" \
    ntfy.sh/flowers
ntfy publish \
    --file=flower.jpg \
    flowers
PUT /flowers HTTP/1.1
Host: ntfy.sh
Filename: flower.jpg
Content-Type: 52312

(binary JPEG data)
fetch('https://ntfy.sh/flowers', {
    method: 'PUT',
    body: document.getElementById("file").files[0],
    headers: { 'Filename': 'flower.jpg' }
})
file, _ := os.Open("flower.jpg")
req, _ := http.NewRequest("PUT", "https://ntfy.sh/flowers", file)
req.Header.Set("Filename", "flower.jpg")
http.DefaultClient.Do(req)
$Request = @{
  Method = "POST"
  Uri = "ntfy.sh/flowers"
  InFile = "flower.jpg"
  Headers = @{"Filename" = "flower.jpg"}
}
Invoke-RestMethod @Request
requests.put("https://ntfy.sh/flowers",
    data=open("flower.jpg", 'rb'),
    headers={ "Filename": "flower.jpg" })
file_get_contents('https://ntfy.sh/flowers', false, stream_context_create([
    'http' => [
        'method' => 'PUT',
        'header' =>
            "Content-Type: application/octet-stream\r\n" . // Does not matter
            "Filename: flower.jpg",
        'content' => file_get_contents('flower.jpg') // Dangerous for large files 
    ]
]));

Here's what that looks like on Android:

image attachment

Image attachment sent from a local file

Attach file from a URL

Instead of sending a local file to your phone, you can use an external URL to specify where the attachment is hosted. This could be a Dropbox link, a file from social media, or any other publicly available URL. Since the files are externally hosted, the expiration or size limits from above do not apply here.

To attach an external file, simple pass the X-Attach header or query parameter (or any of its aliases Attach or a) to specify the attachment URL. It can be any type of file.

ntfy will automatically try to derive the file name from the URL (e.g https://example.com/flower.jpg will yield a filename flower.jpg). To override this filename, you may send the X-Filename header or query parameter (or any of its aliases Filename, File or f).

Here's an example showing how to attach an APK file:

curl \
    -X POST \
    -H "Attach: https://f-droid.org/F-Droid.apk" \
    ntfy.sh/mydownloads
ntfy publish \
    --attach="https://f-droid.org/F-Droid.apk" \
    mydownloads
POST /mydownloads HTTP/1.1
Host: ntfy.sh
Attach: https://f-droid.org/F-Droid.apk
fetch('https://ntfy.sh/mydownloads', {
    method: 'POST',
    headers: { 'Attach': 'https://f-droid.org/F-Droid.apk' }
})
req, _ := http.NewRequest("POST", "https://ntfy.sh/mydownloads", file)
req.Header.Set("Attach", "https://f-droid.org/F-Droid.apk")
http.DefaultClient.Do(req)
$Request = @{
  Method = "POST"
  URI = "https://ntfy.sh/mydownloads"
  Headers = @{ Attach="https://f-droid.org/F-Droid.apk" }
}
Invoke-RestMethod @Request
requests.put("https://ntfy.sh/mydownloads",
    headers={ "Attach": "https://f-droid.org/F-Droid.apk" })
file_get_contents('https://ntfy.sh/mydownloads', false, stream_context_create([
    'http' => [
    'method' => 'PUT',
    'header' =>
        "Content-Type: text/plain\r\n" . // Does not matter
        "Attach: https://f-droid.org/F-Droid.apk",
    ]
]));

file attachment

File attachment sent from an external URL

Icons

Supported on:

You can include an icon that will appear next to the text of the notification. Simply pass the X-Icon header or query parameter (or its alias Icon) to specify the URL that the icon is located at. The client will automatically download the icon (unless it is already cached locally, and less than 24 hours old), and show it in the notification. Icons are cached locally in the client until the notification is deleted. Only JPEG and PNG images are supported at this time.

Here's an example showing how to include an icon:

curl \
    -H "Icon: https://styles.redditmedia.com/t5_32uhe/styles/communityIcon_xnt6chtnr2j21.png" \
    -H "Title: Kodi: Resuming Playback" \
    -H "Tags: arrow_forward" \
    -d "The Wire, S01E01" \
    ntfy.sh/tvshows
ntfy publish \
    --icon="https://styles.redditmedia.com/t5_32uhe/styles/communityIcon_xnt6chtnr2j21.png" \
    --title="Kodi: Resuming Playback" \
    --tags="arrow_forward" \
    tvshows \
    "The Wire, S01E01"
POST /tvshows HTTP/1.1
Host: ntfy.sh
Icon: https://styles.redditmedia.com/t5_32uhe/styles/communityIcon_xnt6chtnr2j21.png
Tags: arrow_forward
Title: Kodi: Resuming Playback

The Wire, S01E01
fetch('https://ntfy.sh/tvshows', {
    method: 'POST',
    headers: { 
        'Icon': 'https://styles.redditmedia.com/t5_32uhe/styles/communityIcon_xnt6chtnr2j21.png',
        'Title': 'Kodi: Resuming Playback',
        'Tags': 'arrow_forward'
    },
    body: "The Wire, S01E01"
})
req, _ := http.NewRequest("POST", "https://ntfy.sh/tvshows", strings.NewReader("The Wire, S01E01"))
req.Header.Set("Icon", "https://styles.redditmedia.com/t5_32uhe/styles/communityIcon_xnt6chtnr2j21.png")
req.Header.Set("Tags", "arrow_forward")
req.Header.Set("Title", "Kodi: Resuming Playback")
http.DefaultClient.Do(req)
$Request = @{
  Method = "POST"
  URI = "https://ntfy.sh/tvshows"
  Headers = @{
    Title = "Kodi: Resuming Playback"
    Tags = "arrow_forward"
    Icon = "https://styles.redditmedia.com/t5_32uhe/styles/communityIcon_xnt6chtnr2j21.png"
  }
  Body = "The Wire, S01E01"
}
Invoke-RestMethod @Request
requests.post("https://ntfy.sh/tvshows",
    data="The Wire, S01E01",
    headers={
        "Title": "Kodi: Resuming Playback",
        "Tags": "arrow_forward",
        "Icon": "https://styles.redditmedia.com/t5_32uhe/styles/communityIcon_xnt6chtnr2j21.png"
    })
file_get_contents('https://ntfy.sh/tvshows', false, stream_context_create([
    'http' => [
    'method' => 'PUT',
    'header' =>
        "Content-Type: text/plain\r\n" . // Does not matter
        "Title: Kodi: Resuming Playback\r\n" .
        "Tags: arrow_forward\r\n" .
        "Icon: https://styles.redditmedia.com/t5_32uhe/styles/communityIcon_xnt6chtnr2j21.png",
    ],
    'content' => "The Wire, S01E01"
]));

Here's an example of how it will look on Android:

file attachment

Custom icon from an external URL

E-mail notifications

Supported on:

You can forward messages to e-mail by specifying an address in the header. This can be useful for messages that you'd like to persist longer, or to blast-notify yourself on all possible channels.

Usage is easy: Simply pass the X-Email header (or any of its aliases: X-E-mail, Email, E-mail, Mail, or e). Only one e-mail address is supported.

Since ntfy does not provide auth (yet), the rate limiting is pretty strict (see limitations). In the default configuration, you get 16 e-mails per visitor (IP address) and then after that one per hour. On top of that, your IP address appears in the e-mail body. This is to prevent abuse.

curl \
    -H "Email: phil@example.com" \
    -H "Tags: warning,skull,backup-host,ssh-login" \
    -H "Priority: high" \
    -d "Unknown login from 5.31.23.83 to backups.example.com" \
    ntfy.sh/alerts
curl -H "Email: phil@example.com" -d "You've Got Mail" 
curl -d "You've Got Mail" "ntfy.sh/alerts?email=phil@example.com"
ntfy publish \
    --email=phil@example.com \
    --tags=warning,skull,backup-host,ssh-login \
    --priority=high \
    alerts "Unknown login from 5.31.23.83 to backups.example.com"
POST /alerts HTTP/1.1
Host: ntfy.sh
Email: phil@example.com
Tags: warning,skull,backup-host,ssh-login
Priority: high

Unknown login from 5.31.23.83 to backups.example.com
fetch('https://ntfy.sh/alerts', {
    method: 'POST',
    body: "Unknown login from 5.31.23.83 to backups.example.com",
    headers: { 
        'Email': 'phil@example.com',
        'Tags': 'warning,skull,backup-host,ssh-login',
        'Priority': 'high'
    }
})
req, _ := http.NewRequest("POST", "https://ntfy.sh/alerts", 
    strings.NewReader("Unknown login from 5.31.23.83 to backups.example.com"))
req.Header.Set("Email", "phil@example.com")
req.Header.Set("Tags", "warning,skull,backup-host,ssh-login")
req.Header.Set("Priority", "high")
http.DefaultClient.Do(req)
$Request = @{
  Method = "POST"
  URI = "https://ntfy.sh/alerts"
  Headers = @{
    Title = "Low disk space alert"
    Priority = "high"
    Tags = "warning,skull,backup-host,ssh-login")
    Email = "phil@example.com"
  }
  Body = "Unknown login from 5.31.23.83 to backups.example.com"
}
Invoke-RestMethod @Request
requests.post("https://ntfy.sh/alerts",
    data="Unknown login from 5.31.23.83 to backups.example.com",
    headers={ 
        "Email": "phil@example.com",
        "Tags": "warning,skull,backup-host,ssh-login",
        "Priority": "high"
    })
file_get_contents('https://ntfy.sh/alerts', false, stream_context_create([
    'http' => [
        'method' => 'POST',
        'header' =>
            "Content-Type: text/plain\r\n" .
            "Email: phil@example.com\r\n" .
            "Tags: warning,skull,backup-host,ssh-login\r\n" .
            "Priority: high",
        'content' => 'Unknown login from 5.31.23.83 to backups.example.com'
    ]
]));

Here's what that looks like in Google Mail:

e-mail notification

E-mail notification

E-mail publishing

Supported on:

You can publish messages to a topic via e-mail, i.e. by sending an email to a specific address. For instance, you can publish a message to the topic sometopic by sending an e-mail to ntfy-sometopic@ntfy.sh. This is useful for e-mail based integrations such as for statuspage.io (though these days most services also support webhooks and HTTP calls).

Depending on the server configuration, the e-mail address format can have a prefix to prevent spam on topics. For ntfy.sh, the prefix is configured to ntfy-, meaning that the general e-mail address format is:

ntfy-$topic@ntfy.sh

If access control is enabled, and the target topic does not support anonymous writes, e-mail publishing won't work without providing an authorized access token. That will change the format of the e-mail's recipient address to

ntfy-$topic+$token@ntfy.sh

As of today, e-mail publishing only supports adding a message title (the e-mail subject). Tags, priority, delay and other features are not supported (yet). Here's an example that will publish a message with the title You've Got Mail to topic sometopic (see ntfy.sh/sometopic):

e-mail publishing

Publishing a message via e-mail

Phone calls

Supported on:

You can use ntfy to call a phone and read the message out loud using text-to-speech. Similar to email notifications, this can be useful to blast-notify yourself on all possible channels, or to notify people that do not have the ntfy app installed on their phone.

Phone numbers have to be previously verified (via the web app), so this feature is only available to authenticated users (no anonymous phone calls). To forward a message as a voice call, pass a phone number in the X-Call header (or its alias: Call), prefixed with a plus sign and the country code, e.g. +12223334444. You may also simply pass yes as a value to pick the first of your verified phone numbers. On ntfy.sh, this feature is only supported to ntfy Pro plans.

phone number verification

Phone number verification in the web app

As of today, the text-to-speed voice used will only support English. If there is demand for other languages, we'll be happy to add support for that. Please open an issue on GitHub.

Info

You are responsible for the message content, and you must abide by the Twilio Acceptable Use Policy. This particularly means that you must not use this feature to send unsolicited messages, or messages that are illegal or violate the rights of others. Please read the policy for details. Failure to do so may result in your account being suspended or terminated.

Here's how you use it:

curl \
    -u :tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2 \
    -H "Call: +12223334444" \
    -d "Your garage seems to be on fire. You should probably check that out." \
    ntfy.sh/alerts
ntfy publish \
    --token=tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2 \
    --call=+12223334444 \
    alerts "Your garage seems to be on fire. You should probably check that out."
POST /alerts HTTP/1.1
Host: ntfy.sh
Authorization: Bearer tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2
Call: +12223334444

Your garage seems to be on fire. You should probably check that out.
fetch('https://ntfy.sh/alerts', {
    method: 'POST',
    body: "Your garage seems to be on fire. You should probably check that out.",
    headers: { 
        'Authorization': 'Bearer tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2',
        'Call': '+12223334444'
    }
})
req, _ := http.NewRequest("POST", "https://ntfy.sh/alerts", 
    strings.NewReader("Your garage seems to be on fire. You should probably check that out."))
req.Header.Set("Call", "+12223334444")
req.Header.Set("Authorization", "Bearer tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2")
http.DefaultClient.Do(req)
$Request = @{
  Method = "POST"
  URI = "https://ntfy.sh/alerts"
  Headers = @{
    Authorization = "Bearer tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2"
    Call = "+12223334444"
  }
  Body = "Your garage seems to be on fire. You should probably check that out."
}
Invoke-RestMethod @Request
requests.post("https://ntfy.sh/alerts",
    data="Your garage seems to be on fire. You should probably check that out.",
    headers={ 
        "Authorization": "Bearer tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2",
        "Call": "+12223334444"
    })
file_get_contents('https://ntfy.sh/alerts', false, stream_context_create([
    'http' => [
        'method' => 'POST',
        'header' =>
            "Content-Type: text/plain\r\n" .
            "Authorization: Bearer tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2\r\n" .
            "Call: +12223334444",
        'content' => 'Your garage seems to be on fire. You should probably check that out.'
    ]
]));

Here's what a phone call from ntfy sounds like:

Audio transcript:

You have a notification from ntfy on topic alerts.
Message: Your garage seems to be on fire. You should probably check that out. End message.
This message was sent by user phil. It will be repeated up to three times.

Authentication

Depending on whether the server is configured to support access control, some topics may be read/write protected so that only users with the correct credentials can subscribe or publish to them. To publish/subscribe to protected topics, you can:

  • Use username & password via Basic auth, e.g. Authorization: Basic dGVzdHVzZXI6ZmFrZXBhc3N3b3Jk
  • Use access tokens via Bearer/Basic auth, e.g. Authorization: Bearer tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2
  • or use either with the auth query parameter, e.g. ?auth=QmFzaWMgZEdWemRIVnpaWEk2Wm1GclpYQmhjM04zYjNKaw

Warning

When using Basic auth, base64 only encodes username and password. It is not encrypting it. For your self-hosted server, be sure to use HTTPS to avoid eavesdropping and exposing your password.

Username + password

The simplest way to authenticate against a ntfy server is to use Basic auth. Here's an example with a user testuser and password fakepassword:

curl \
  -u testuser:fakepassword \
  -d "Look ma, with auth" \
  https://ntfy.example.com/mysecrets
ntfy publish \
  -u testuser:fakepassword \
  ntfy.example.com/mysecrets \
  "Look ma, with auth"
POST /mysecrets HTTP/1.1
Host: ntfy.example.com
Authorization: Basic dGVzdHVzZXI6ZmFrZXBhc3N3b3Jk

Look ma, with auth
fetch('https://ntfy.example.com/mysecrets', {
    method: 'POST', // PUT works too
    body: 'Look ma, with auth',
    headers: {
        'Authorization': 'Basic dGVzdHVzZXI6ZmFrZXBhc3N3b3Jk'
    }
})
req, _ := http.NewRequest("POST", "https://ntfy.example.com/mysecrets",
strings.NewReader("Look ma, with auth"))
req.Header.Set("Authorization", "Basic dGVzdHVzZXI6ZmFrZXBhc3N3b3Jk")
http.DefaultClient.Do(req)
# Get the credentials from the user
$Credential = Get-Credential testuser

# Alternatively, create a PSCredential object with the password from scratch
$Credential = [PSCredential]::new("testuser", (ConvertTo-SecureString "password" -AsPlainText -Force))

# Note that the Authentication parameter requires PowerShell 7 or later
$Request = @{
  Method = "POST"
  URI = "https://ntfy.example.com/mysecrets"
  Authentication = "Basic"
  Credential = $Credential
  Body = "Look ma, with auth"
}
Invoke-RestMethod @Request
# With PowerShell 5 or earlier, we need to create the base64 username:password string ourselves
$CredentialString = "$($Credential.Username):$($Credential.GetNetworkCredential().Password)"
$EncodedCredential = [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($CredentialString))
$Request = @{
  Method = "POST"
  URI = "https://ntfy.example.com/mysecrets"
  Headers = @{ Authorization = "Basic $EncodedCredential"}
  Body = "Look ma, with auth"
}
Invoke-RestMethod @Request
requests.post("https://ntfy.example.com/mysecrets",
data="Look ma, with auth",
headers={
    "Authorization": "Basic dGVzdHVzZXI6ZmFrZXBhc3N3b3Jk"
})
file_get_contents('https://ntfy.example.com/mysecrets', false, stream_context_create([
    'http' => [
        'method' => 'POST', // PUT also works
        'header' =>
            'Content-Type: text/plain\r\n' .
            'Authorization: Basic dGVzdHVzZXI6ZmFrZXBhc3N3b3Jk',
        'content' => 'Look ma, with auth'
    ]
]));

To generate the Authorization header, use standard base64 to encode the colon-separated <username>:<password> and prepend the word Basic, i.e. Authorization: Basic base64(<username>:<password>). Here's some pseudo-code that hopefully explains it better:

username   = "testuser"
password   = "fakepassword"
authHeader = "Basic " + base64(username + ":" + password) // -> Basic dGVzdHVzZXI6ZmFrZXBhc3N3b3Jk

The following command will generate the appropriate value for you on *nix systems:

echo "Basic $(echo -n 'testuser:fakepassword' | base64)"

Access tokens

In addition to username/password auth, ntfy also provides authentication via access tokens. Access tokens are useful to avoid having to configure your password across multiple publishing/subscribing applications. For instance, you may want to use a dedicated token to publish from your backup host, and one from your home automation system.

You can create access tokens using the ntfy token command, or in the web app in the "Account" section (when logged in). See access tokens for details.

Once an access token is created, you can use it to authenticate against the ntfy server, e.g. when you publish or subscribe to topics. Here's an example using Bearer auth, with the token tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2:

curl \
  -H "Authorization: Bearer tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2" \
  -d "Look ma, with auth" \
  https://ntfy.example.com/mysecrets
ntfy publish \
  --token tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2 \
  ntfy.example.com/mysecrets \
  "Look ma, with auth"
POST /mysecrets HTTP/1.1
Host: ntfy.example.com
Authorization: Bearer tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2

Look ma, with auth
fetch('https://ntfy.example.com/mysecrets', {
    method: 'POST', // PUT works too
    body: 'Look ma, with auth',
    headers: {
        'Authorization': 'Bearer tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2'
    }
})
req, _ := http.NewRequest("POST", "https://ntfy.example.com/mysecrets",
strings.NewReader("Look ma, with auth"))
req.Header.Set("Authorization", "Bearer tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2")
http.DefaultClient.Do(req)
# With PowerShell 7 or greater, we can use the Authentication and Token parameters
# The Token parameter must be in the form of a System.Security.SecureString

$Request = @{
  Method = "POST"
  URI = "https://ntfy.example.com/mysecrets"
  Authentication = "Bearer"
  Token = ConvertTo-SecureString "tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2" -AsPlainText
  Body = "Look ma, with auth"
}
Invoke-RestMethod @Request
# In PowerShell 5 and below, we can only send the Bearer token as a string in the Headers
$Request = @{
  Method = "POST"
  URI = "https://ntfy.example.com/mysecrets"
  Headers = @{ Authorization = "Bearer tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2" }
  Body = "Look ma, with auth"
}
Invoke-RestMethod @Request
requests.post("https://ntfy.example.com/mysecrets",
data="Look ma, with auth",
headers={
    "Authorization": "Bearer tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2"
})
file_get_contents('https://ntfy.example.com/mysecrets', false, stream_context_create([
    'http' => [
        'method' => 'POST', // PUT also works
        'header' =>
            'Content-Type: text/plain\r\n' .
            'Authorization: Bearer tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2',
        'content' => 'Look ma, with auth'
    ]
]));

Alternatively, you can use Basic Auth to send the access token. When sending an empty username, the basic auth password is treated by the ntfy server as an access token. This is primarily useful to make curl calls easier, e.g. curl -u:tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2 ...:

curl \
  -u :tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2 \
  -d "Look ma, with auth" \
  https://ntfy.example.com/mysecrets
ntfy publish \
  --token tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2 \
  ntfy.example.com/mysecrets \
  "Look ma, with auth"
POST /mysecrets HTTP/1.1
Host: ntfy.example.com
Authorization: Basic OnRrX0FnUWRxN21WQm9GRDM3elFWTjI5Umh1TXpOSXoy

Look ma, with auth
fetch('https://ntfy.example.com/mysecrets', {
    method: 'POST', // PUT works too
    body: 'Look ma, with auth',
    headers: {
        'Authorization': 'Basic OnRrX0FnUWRxN21WQm9GRDM3elFWTjI5Umh1TXpOSXoy'
    }
})
req, _ := http.NewRequest("POST", "https://ntfy.example.com/mysecrets",
strings.NewReader("Look ma, with auth"))
req.Header.Set("Authorization", "Basic OnRrX0FnUWRxN21WQm9GRDM3elFWTjI5Umh1TXpOSXoy")
http.DefaultClient.Do(req)
# Note that PSCredentials *must* have a username, so we fall back to placing the authorization in the Headers as with PowerShell 5
$Request = @{
  Method = "POST"
  URI = "https://ntfy.example.com/mysecrets"
  Headers = @{
    Authorization = "Basic OnRrX0FnUWRxN21WQm9GRDM3elFWTjI5Umh1TXpOSXoy"
  }
  Body = "Look ma, with auth"
}
Invoke-RestMethod @Request
requests.post("https://ntfy.example.com/mysecrets",
data="Look ma, with auth",
headers={
    "Authorization": "Basic OnRrX0FnUWRxN21WQm9GRDM3elFWTjI5Umh1TXpOSXoy"
})
file_get_contents('https://ntfy.example.com/mysecrets', false, stream_context_create([
    'http' => [
        'method' => 'POST', // PUT also works
        'header' =>
            'Content-Type: text/plain\r\n' .
            'Authorization: Basic OnRrX0FnUWRxN21WQm9GRDM3elFWTjI5Umh1TXpOSXoy',
        'content' => 'Look ma, with auth'
    ]
]));

Query param

Here's an example using the auth query parameter:

curl \
  -d "Look ma, with auth" \
  "https://ntfy.example.com/mysecrets?auth=QmFzaWMgZEdWemRIVnpaWEk2Wm1GclpYQmhjM04zYjNKaw"
ntfy publish \
  -u testuser:fakepassword \
  ntfy.example.com/mysecrets \
  "Look ma, with auth"
POST /mysecrets?auth=QmFzaWMgZEdWemRIVnpaWEk2Wm1GclpYQmhjM04zYjNKaw HTTP/1.1
Host: ntfy.example.com

Look ma, with auth
fetch('https://ntfy.example.com/mysecrets?auth=QmFzaWMgZEdWemRIVnpaWEk2Wm1GclpYQmhjM04zYjNKaw', {
    method: 'POST', // PUT works too
    body: 'Look ma, with auth'
})
req, _ := http.NewRequest("POST", "https://ntfy.example.com/mysecrets?auth=QmFzaWMgZEdWemRIVnpaWEk2Wm1GclpYQmhjM04zYjNKaw",
    strings.NewReader("Look ma, with auth"))
http.DefaultClient.Do(req)
$Request = @{
  Method = "POST"
  URI = "https://ntfy.example.com/mysecrets?auth=QmFzaWMgZEdWemRIVnpaWEk2Wm1GclpYQmhjM04zYjNKaw"
  Body = "Look ma, with auth"
}
Invoke-RestMethod @Request
requests.post("https://ntfy.example.com/mysecrets?auth=QmFzaWMgZEdWemRIVnpaWEk2Wm1GclpYQmhjM04zYjNKaw",
data="Look ma, with auth"
file_get_contents('https://ntfy.example.com/mysecrets?auth=QmFzaWMgZEdWemRIVnpaWEk2Wm1GclpYQmhjM04zYjNKaw', false, stream_context_create([
    'http' => [
        'method' => 'POST', // PUT also works
        'header' => 'Content-Type: text/plain',
        'content' => 'Look ma, with auth'
    ]
]));

To generate the value of the auth parameter, encode the value of the Authorization header (see above) using raw base64 encoding (like base64, but strip any trailing =). Here's some pseudo-code that hopefully explains it better:

username   = "testuser"
password   = "fakepassword"
authHeader = "Basic " + base64(username + ":" + password) // -> Basic dGVzdHVzZXI6ZmFrZXBhc3N3b3Jk
authParam  = base64_raw(authHeader) // -> QmFzaWMgZEdWemRIVnpaWEk2Wm1GclpYQmhjM04zYjNKaw (no trailing =)

// If your language does not have a function to encode raw base64, simply use normal base64
// and REMOVE TRAILING "=" characters. 

The following command will generate the appropriate value for you on *nix systems:

echo -n "Basic `echo -n 'testuser:fakepassword' | base64`" | base64 | tr -d '='

For access tokens, you can use this instead:

echo -n "Bearer faketoken" | base64 | tr -d '='

Advanced features

Message caching

Info

If Cache: no is used, messages will only be delivered to connected subscribers, and won't be re-delivered if a client re-connects. If a subscriber has (temporary) network issues or is reconnecting momentarily, messages might be missed.

By default, the ntfy server caches messages on disk for 12 hours (see message caching), so all messages you publish are stored server-side for a little while. The reason for this is to overcome temporary client-side network disruptions, but arguably this feature also may raise privacy concerns.

To avoid messages being cached server-side entirely, you can set X-Cache header (or its alias: Cache) to no. This will make sure that your message is not cached on the server, even if server-side caching is enabled. Messages are still delivered to connected subscribers, but since= and poll=1 won't return the message anymore.

curl -H "X-Cache: no" -d "This message won't be stored server-side" ntfy.sh/mytopic
curl -H "Cache: no" -d "This message won't be stored server-side" ntfy.sh/mytopic
ntfy publish \
    --no-cache \
    mytopic "This message won't be stored server-side"
POST /mytopic HTTP/1.1
Host: ntfy.sh
Cache: no

This message won't be stored server-side
fetch('https://ntfy.sh/mytopic', {
    method: 'POST',
    body: 'This message won't be stored server-side',
    headers: { 'Cache': 'no' }
})
req, _ := http.NewRequest("POST", "https://ntfy.sh/mytopic", strings.NewReader("This message won't be stored server-side"))
req.Header.Set("Cache", "no")
http.DefaultClient.Do(req)
$Request = @{
  Method = "POST"
  URI = "https://ntfy.sh/mytopic"
  Headers = @{ Cache="no" }
  Body = "This message won't be stored server-side"
}
Invoke-RestMethod @Request
requests.post("https://ntfy.sh/mytopic",
    data="This message won't be stored server-side",
    headers={ "Cache": "no" })
file_get_contents('https://ntfy.sh/mytopic', false, stream_context_create([
    'http' => [
        'method' => 'POST',
        'header' =>
            "Content-Type: text/plain\r\n" .
            "Cache: no",
        'content' => 'This message won't be stored server-side'
    ]
]));

Disable Firebase

Info

If Firebase: no is used and instant delivery isn't enabled in the Android app (Google Play variant only), message delivery will be significantly delayed (up to 15 minutes). To overcome this delay, simply enable instant delivery.

The ntfy server can be configured to use Firebase Cloud Messaging (FCM) (see Firebase config) for message delivery on Android (to minimize the app's battery footprint). The ntfy.sh server is configured this way, meaning that all messages published to ntfy.sh are also published to corresponding FCM topics.

If you'd like to avoid forwarding messages to Firebase, you can set the X-Firebase header (or its alias: Firebase) to no. This will instruct the server not to forward messages to Firebase.

curl -H "X-Firebase: no" -d "This message won't be forwarded to FCM" ntfy.sh/mytopic
curl -H "Firebase: no" -d "This message won't be forwarded to FCM" ntfy.sh/mytopic
ntfy publish \
    --no-firebase \
    mytopic "This message won't be forwarded to FCM"
POST /mytopic HTTP/1.1
Host: ntfy.sh
Firebase: no

This message won't be forwarded to FCM
fetch('https://ntfy.sh/mytopic', {
    method: 'POST',
    body: 'This message won't be forwarded to FCM',
    headers: { 'Firebase': 'no' }
})
req, _ := http.NewRequest("POST", "https://ntfy.sh/mytopic", strings.NewReader("This message won't be forwarded to FCM"))
req.Header.Set("Firebase", "no")
http.DefaultClient.Do(req)
$Request = @{
  Method = "POST"
  URI = "https://ntfy.sh/mytopic"
  Headers = @{ Firebase="no" }
  Body = "This message won't be forwarded to FCM"
}
Invoke-RestMethod @Request
requests.post("https://ntfy.sh/mytopic",
    data="This message won't be forwarded to FCM",
    headers={ "Firebase": "no" })
file_get_contents('https://ntfy.sh/mytopic', false, stream_context_create([
    'http' => [
        'method' => 'POST',
        'header' =>
            "Content-Type: text/plain\r\n" .
            "Firebase: no",
        'content' => 'This message won't be stored server-side'
    ]
]));

UnifiedPush

Info

This setting is not relevant to users, only to app developers and people interested in UnifiedPush.

UnifiedPush is a standard for receiving push notifications without using the Google-owned Firebase Cloud Messaging (FCM) service. It puts push notifications in the control of the user. ntfy can act as a UnifiedPush distributor, forwarding messages to apps that support it.

When publishing messages to a topic, apps using ntfy as a UnifiedPush distributor can set the X-UnifiedPush header or query parameter (or any of its aliases unifiedpush or up) to 1 to disable Firebase. As of today, this option is mostly equivalent to Firebase: no, but was introduced to allow future flexibility. The flag additionally enables auto-detection of the message encoding. If the message is binary, it'll be encoded as base64.

Matrix Gateway

The ntfy server implements a Matrix Push Gateway (in combination with UnifiedPush as the Provider Push Protocol). This makes it easier to integrate with self-hosted Matrix servers (such as synapse), since you don't have to set up a separate push proxy (such as common-proxies).

In short, ntfy accepts Matrix messages on the /_matrix/push/v1/notify endpoint (see Push Gateway API), and forwards them to the ntfy topic defined in the pushkey of the message. The message will then be forwarded to the ntfy Android app, and passed on to the Matrix client there.

There is a nice diagram in the Push Gateway docs. In this diagram, the ntfy server plays the role of the Push Gateway, as well as the Push Provider. UnifiedPush is the Provider Push Protocol.

Info

This is not a generic Matrix Push Gateway. It only works in combination with UnifiedPush and ntfy.

Public topics

Obviously all topics on ntfy.sh are public, but there are a few designated topics that are used in examples, and topics that you can use to try out what authentication and access control looks like.

Topic User Permissions Description
announcements * (unauthenticated) Read-only for everyone Release announcements and such
stats * (unauthenticated) Read-only for everyone Daily statistics about ntfy.sh usage

Limitations

There are a few limitations to the API to prevent abuse and to keep the server healthy. Almost all of these settings are configurable via the server side rate limiting settings. Most of these limits you won't run into, but just in case, let's list them all:

Limit Description
Message length Each message can be up to 4,096 bytes long. Longer messages are treated as attachments.
Requests By default, the server is configured to allow 60 requests per visitor at once, and then refills the your allowed requests bucket at a rate of one request per 5 seconds.
Daily messages By default, the number of messages is governed by the request limits. This can be overridden. On ntfy.sh, the daily message limit is 250.
E-mails By default, the server is configured to allow sending 16 e-mails per visitor at once, and then refills the your allowed e-mail bucket at a rate of one per hour. On ntfy.sh, the daily limit is 5.
Phone calls By default, the server does not allow any phone calls, except for users with a tier that has a call limit.
Subscription limit By default, the server allows each visitor to keep 30 connections to the server open.
Attachment size limit By default, the server allows attachments up to 15 MB in size, up to 100 MB in total per visitor and up to 5 GB across all visitors. On ntfy.sh, the attachment size limit is 2 MB, and the per-visitor total is 20 MB.
Attachment expiry By default, the server deletes attachments after 3 hours and thereby frees up space from the total visitor attachment limit.
Attachment bandwidth By default, the server allows 500 MB of GET/PUT/POST traffic for attachments per visitor in a 24 hour period. Traffic exceeding that is rejected. On ntfy.sh, the daily bandwidth limit is 200 MB.
Total number of topics By default, the server is configured to allow 15,000 topics. The ntfy.sh server has higher limits though.

These limits can be changed on a per-user basis using tiers. If payments are enabled, a user tier can be changed by purchasing a higher tier. ntfy.sh offers multiple paid tiers, which allows for much hier limits than the ones listed above.

List of all parameters

The following is a list of all parameters that can be passed when publishing a message. Parameter names are case-insensitive when used in HTTP headers, and must be lowercase when used as query parameters in the URL. They are listed in the table in their canonical form.

Info

ntfy supports UTF-8 in HTTP headers, but not every library or programming language does. If non-ASCII characters are causing issues for you in the title (i.e. you're seeing ? symbols), you may also encode any header as RFC 2047, e.g. =?UTF-8?B?8J+HqfCfh6o=?= (base64), or =?UTF-8?Q?=C3=84pfel?= (quoted-printable).

Parameter Aliases Description
X-Message Message, m Main body of the message as shown in the notification
X-Title Title, t Message title
X-Priority Priority, prio, p Message priority
X-Tags Tags, Tag, ta Tags and emojis
X-Delay Delay, X-At, At, X-In, In Timestamp or duration for delayed delivery
X-Actions Actions, Action JSON array or short format of user actions
X-Click Click URL to open when notification is clicked
X-Attach Attach, a URL to send as an attachment, as an alternative to PUT/POST-ing an attachment
X-Markdown Markdown, md Enable Markdown formatting in the notification body
X-Icon Icon URL to use as notification icon
X-Filename Filename, file, f Optional attachment filename, as it appears in the client
X-Email X-E-Mail, Email, E-Mail, mail, e E-mail address for e-mail notifications
X-Call Call Phone number for phone calls
X-Cache Cache Allows disabling message caching
X-Firebase Firebase Allows disabling sending to Firebase
X-UnifiedPush UnifiedPush, up UnifiedPush publish option, only to be used by UnifiedPush apps
X-Poll-ID Poll-ID Internal parameter, used for iOS push notifications
Authorization - If supported by the server, you can login to access protected topics
Content-Type - If set to text/markdown, Markdown formatting is enabled