A web page on your home Wi-Fi only works while you are at home. To get an alert anywhere, the ESP32 has to send it through a service on the internet. ntfy (say "notify") is a free, open-source service that does exactly that: the ESP32 sends a short message to a topic, and the ntfy app on your phone shows it as a notification. You don't even need an account. In this lesson the ESP32 watches a PIR motion sensor and sends you an alert when something moves.
What you'll learn
- How to send a message to an online service with an HTTPS POST request
- What a publish/subscribe topic is
- How to stop a sensor flooding you with alerts
- Why a secret topic name matters
What you need
- ESP32 board, expansion board and USB cable (ESP32 IoT Starter Kit)
- PIR motion sensor (ESP32 IoT Starter Kit)
- 1 LED and 1 resistor, 220 Ω (ESP32 IoT Starter Kit)
- 1 push button (ESP32 IoT Starter Kit)
- A phone with the free ntfy app (Android or iPhone)
- A 2.4 GHz Wi-Fi network
- The printed parts from the 3D printing box below (PIR sensor case), or the cardboard version described there
- Breadboard and jumper wires (ESP32 IoT Starter Kit)
Wiring

| From | To |
|---|---|
| PIR VCC | ESP32 5V |
| PIR OUT | ESP32 GPIO 27 |
| PIR GND | GND |
| ESP32 GPIO 25 | 220 Ω resistor, then the LED's long leg |
| LED short leg | GND |
| Push button, one leg | ESP32 GPIO 26 |
| Push button, the diagonally opposite leg | GND |
The PIR sensor needs 5 V to power it, but its OUT pin only gives 3.3 V, which is safe for the ESP32.
Set up ntfy
- Install the ntfy app from Google Play or the App Store and open it.
- Make up a topic name that nobody could guess, such as
eementor-hall-7f3k9q. Use only letters, numbers, - and _. - In the app, tap + (Subscribe to topic), type your topic name and tap Subscribe.
- Test it: open
https://ntfy.sh/your-topic-namein a browser on your computer, type a message at the bottom and send it. It should pop up on your phone.
Important: on the free ntfy.sh server, topics are public. Anyone who knows your topic name can read your messages and send you their own. A long, random name keeps it private enough for a motion alarm, but never send passwords or personal details through it.
The sketch
Replace YOUR_WIFI_NAME and YOUR_WIFI_PASSWORD with your own network details (2.4 GHz only).
Put your topic name in TOPIC. Everything else is built into the ESP32 board package.
#include <WiFi.h>
#include <WiFiClientSecure.h>
#include <HTTPClient.h>
const char* WIFI_NAME = "YOUR_WIFI_NAME";
const char* WIFI_PASSWORD = "YOUR_WIFI_PASSWORD";
const char* TOPIC = "YOUR_SECRET_TOPIC"; // for example eementor-hall-7f3k9q
const int PIR_PIN = 27;
const int LED_PIN = 25; // on while the alarm is armed
const int BUTTON_PIN = 26; // press to arm or disarm
const unsigned long QUIET_TIME = 60000; // at most one motion alert per minute
bool armed = true;
bool lastButton = HIGH;
unsigned long lastAlert = 0;
bool alertedOnce = false;
unsigned long lastWifiTry = 0;
bool notify(const char* title, const char* message, const char* tags) {
WiFiClientSecure client;
client.setInsecure();
HTTPClient http;
http.begin(client, String("https://ntfy.sh/") + TOPIC);
http.addHeader("Title", title);
http.addHeader("Tags", tags); // emoji names, shown in front of the title
http.addHeader("Priority", "high");
int code = http.POST(message);
http.end();
Serial.printf("ntfy answered %d\n", code);
return code == 200;
}
void setup() {
Serial.begin(115200);
pinMode(PIR_PIN, INPUT);
pinMode(LED_PIN, OUTPUT);
pinMode(BUTTON_PIN, INPUT_PULLUP);
WiFi.begin(WIFI_NAME, WIFI_PASSWORD);
Serial.print("Connecting to Wi-Fi");
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println(" connected");
Serial.println("Waiting 30 s for the PIR sensor to settle...");
delay(30000);
digitalWrite(LED_PIN, HIGH);
notify("Motion alarm is on", "Your motion alarm is armed and watching.", "white_check_mark");
}
void loop() {
bool button = digitalRead(BUTTON_PIN);
if (button == LOW && lastButton == HIGH) {
armed = !armed;
digitalWrite(LED_PIN, armed);
Serial.println(armed ? "Armed" : "Disarmed");
notify(armed ? "Motion alarm armed" : "Motion alarm paused",
armed ? "Watching for movement again." : "No alerts until you press the button again.",
armed ? "lock" : "unlock");
delay(200);
}
lastButton = button;
bool quietOver = !alertedOnce || millis() - lastAlert > QUIET_TIME;
if (armed && digitalRead(PIR_PIN) == HIGH && quietOver) {
Serial.println("Motion! Sending an alert");
if (notify("Motion detected", "Something moved in front of the sensor.", "rotating_light")) {
alertedOnce = true;
lastAlert = millis();
}
}
if (WiFi.status() != WL_CONNECTED && millis() - lastWifiTry > 10000) {
lastWifiTry = millis();
WiFi.reconnect();
}
}
Try it
- Upload the sketch and open the Serial Monitor at 115200 baud. After 30 seconds the LED lights and your phone shows "Motion alarm is on".
- Wave in front of the sensor. Within a second or two your phone shows "Motion detected".
- Switch your phone to mobile data and try again: it still works, because the message goes through the internet.
- Press the button to pause the alarm, and again to arm it.
How it works
ntfy works on the publish/subscribe idea. The ESP32 publishes a message to a topic by sending an HTTPS POST request to https://ntfy.sh/your-topic; the text of the message is the body of the request, and the title, emoji tags and priority travel as headers. Your phone subscribes to the same topic and gets every message published to it. Neither side needs to know the other's address.
A PIR sensor stays HIGH for a few seconds after each movement, and a person walking around would trigger it again and again. QUIET_TIME allows at most one alert a minute, so your phone doesn't buzz forty times.
client.setInsecure() keeps the connection encrypted but skips checking the server's certificate. That keeps the sketch short and is fine for a learning project. For a device you leave running for years, load the server's root certificate with client.setCACert() instead.
Try this next
- Count the movements in the quiet minute and send "5 movements in the last minute" instead.
- Add a
Clickheader with a web address, so tapping the notification opens it. - Send a daily "still alive" message at 8:00 with the time from the internet alarm clock.
Troubleshooting
- "ntfy answered -1": the ESP32 couldn't reach the internet. Check that your Wi-Fi has internet access.
- "ntfy answered 429": too many messages in a short time. Wait a minute; the free server limits how often you can publish.
- The phone shows nothing: check the topic name in the sketch and in the app letter by letter. On Android, allow ntfy to show notifications and to run in the background.
- Alerts without movement: PIR sensors react to warm air from heaters and sunlight. Point it away from them, and turn the sensitivity knob down a little.
- It never connects to Wi-Fi: check the network name and password (both are case-sensitive) and make sure the network is 2.4 GHz.
- A printed part is too tight or too loose: printers differ by a few tenths of a millimetre. Sand or file a tight hole, and fix a loose part with a drop of hot glue or a strip of tape.