Learning Goals 5 min
L03-31 had the ESP fetching data. L03-32 had the ESP serving data. Today the ESP pushes data to another service — when a button is pressed, send an HTTP POST to a Discord channel, a Google Sheet, an IFTTT trigger, or your own server. That outbound POST is called a webhook, and it's how your IoT device talks to the rest of the internet. By the end of this lesson you will:
- Define what a webhook is and contrast it with a polling / fetching client.
- Send an HTTP POST with a JSON body to a free webhook endpoint (Discord channel webhook is the easiest in 2026) and see the message appear.
- Tie a real Arduino event (button press, sensor threshold) to a webhook call — your first IoT trigger.
Warm-Up 10 min
Hardware: ESP + one push button on D7 with INPUT_PULLUP. Optionally an LED for confirmation.
What's a webhook
A webhook is just an HTTP POST to a URL that someone else exposes for you to fire. The receiving service has agreed: "if you POST a JSON body matching this shape to this URL, I'll do something." Examples:
- Discord: POST a JSON message → it appears in a channel.
- IFTTT / Zapier: POST a trigger → kicks off a recipe (send SMS, write to Google Sheets, etc.).
- Slack: similar to Discord.
- Your own server: define the schema yourself.
The opposite direction: polling. Your ESP would have to fetch "has anything happened?" every N seconds. With webhooks, the ESP tells the world when something happens — no polling needed.
Discord webhook setup (the friendliest free option)
If you have a Discord server (any server you admin works, including a private one with just you):
- Open your server in Discord. Pick a channel where you want notifications.
- Channel settings → Integrations → Webhooks → New Webhook.
- Name it (e.g. "Doorbell"), choose the channel, copy the Webhook URL.
- The URL is a long secret string like
https://discord.com/api/webhooks/123456789012345/abcDEF.... Treat it like a password — anyone with the URL can post to your channel.
If you don't have Discord, IFTTT has a free Webhooks service: sign in to IFTTT → search the Webhooks integration → get your key → URL becomes https://maker.ifttt.com/trigger/[event]/with/key/[YOUR_KEY].
New Concept · HTTPS + POST with body 25 min
HTTPS is required by Discord
Discord's webhook URLs are https://, not http://. On ESP8266, HTTPS uses ~30 KB of RAM during the handshake and a few seconds of CPU. ESP32 handles it more comfortably. For ESP8266, use WiFiClientSecure with setInsecure() (skip cert verification — fine for testing, not for production secrets).
#if defined(ESP8266)
#include <ESP8266WiFi.h>
#include <ESP8266HTTPClient.h>
#include <WiFiClientSecureBearSSL.h>
using SecureClient = BearSSL::WiFiClientSecure;
#elif defined(ESP32)
#include <WiFi.h>
#include <HTTPClient.h>
#include <WiFiClientSecure.h>
using SecureClient = WiFiClientSecure;
#endifThis using SecureClient = ... trick gives both boards the same type name in our sketch.
Sending a POST with JSON body
bool postWebhook(const char* url, const String& jsonBody) {
if (WiFi.status() != WL_CONNECTED) return false;
SecureClient client;
client.setInsecure(); // skip cert verification (OK for school)
HTTPClient http;
http.begin(client, url);
http.addHeader("Content-Type", "application/json");
int status = http.POST(jsonBody);
Serial.print("# POST -> "); Serial.println(status);
http.end();
return status >= 200 && status < 300;
}Three new pieces:
SecureClient+setInsecure()= TLS without certificate verification.http.addHeader("Content-Type", "application/json")= tell the server how to interpret the body.http.POST(body)= send the request with the body. Returns the status code.
The Discord message format
The simplest Discord webhook payload:
{
"content": "Doorbell pressed at 19:30!"
}
That's it. Discord renders the content field as a chat message in the channel. You can add fancier fields (username override, embeds for cards) but plain content is enough for our doorbell.
Building the JSON body
String body = "{\"content\":\"Doorbell pressed at ";
char timeBuf[20];
time_t now = time(nullptr);
struct tm tmNow;
localtime_r(&now, &tmNow);
strftime(timeBuf, sizeof(timeBuf), "%H:%M:%S", &tmNow);
body += timeBuf;
body += "!\"}";For longer / more structured payloads, prefer ArduinoJson's serializeJson(). For one-line messages, string concatenation is fine.
The trigger side — debouncing the button
You don't want a button bounce to send 5 messages. Debounce + once-per-press dispatch:
const int BTN_PIN = D7;
const unsigned long DEBOUNCE_MS = 50;
bool lastBtn = HIGH;
unsigned long lastEdge = 0;
void checkButton() {
bool b = digitalRead(BTN_PIN);
if (b == lastBtn) return;
if (millis() - lastEdge < DEBOUNCE_MS) return;
lastEdge = millis();
lastBtn = b;
if (b == LOW) { // pressed
onPress();
}
}L01-18 / L01-19 patterns; same idea applied here.
Worked Example · "Doorbell that DMs you" (a teaser of L03-38) 25 min
Step 1 — get a Discord webhook URL
Follow the §2 instructions. Copy the URL.
Step 2 — wire a button
Button between D7 and GND. INPUT_PULLUP in software — no external resistor.
Step 3 — the sketch
// L03-36 · Doorbell webhook (Discord)
#if defined(ESP8266)
#include <ESP8266WiFi.h>
#include <ESP8266HTTPClient.h>
#include <WiFiClientSecureBearSSL.h>
using SecureClient = BearSSL::WiFiClientSecure;
#elif defined(ESP32)
#include <WiFi.h>
#include <HTTPClient.h>
#include <WiFiClientSecure.h>
using SecureClient = WiFiClientSecure;
#endif
#include <time.h>
const char* SSID = "YourNetwork";
const char* PASSWORD = "YourPassword";
const char* WEBHOOK_URL = "https://discord.com/api/webhooks/REPLACE_ME";
const int BTN_PIN = 13; // D7 on NodeMCU
bool lastBtn = HIGH;
unsigned long lastEdge = 0;
const unsigned long DEBOUNCE_MS = 50;
bool postDiscord(const String& message) {
if (WiFi.status() != WL_CONNECTED) return false;
SecureClient client;
client.setInsecure();
HTTPClient http;
http.begin(client, WEBHOOK_URL);
http.addHeader("Content-Type", "application/json");
String body = "{\"content\":\"";
body += message;
body += "\"}";
int status = http.POST(body);
Serial.print("# POST -> "); Serial.println(status);
http.end();
return status >= 200 && status < 300;
}
void onPress() {
time_t now = time(nullptr);
struct tm tmNow;
localtime_r(&now, &tmNow);
char buf[24];
strftime(buf, sizeof(buf), "%H:%M:%S", &tmNow);
String msg = "Doorbell pressed at ";
msg += buf;
msg += "!";
Serial.println(msg);
postDiscord(msg);
}
void checkButton() {
bool b = digitalRead(BTN_PIN);
if (b == lastBtn) return;
if (millis() - lastEdge < DEBOUNCE_MS) return;
lastEdge = millis();
lastBtn = b;
if (b == LOW) onPress();
}
void setup() {
Serial.begin(115200);
pinMode(BTN_PIN, INPUT_PULLUP);
WiFi.mode(WIFI_STA);
WiFi.begin(SSID, PASSWORD);
while (WiFi.status() != WL_CONNECTED) { delay(500); Serial.print("."); }
Serial.println();
Serial.print("# IP = "); Serial.println(WiFi.localIP());
// NTP for the timestamp
configTime(0, 0, "pool.ntp.org");
setenv("TZ", "MYT-8", 1); // adjust to your zone
tzset();
while (time(nullptr) < 24 * 3600) delay(200);
Serial.println("# Ready. Press the button to ring.");
}
void loop() {
checkButton();
}Step 4 — upload + ring the doorbell
Press the button. Within ~1 s, your Discord channel shows "Doorbell pressed at 19:30:01!". Phone notifications fire (if you have Discord on your phone).
Step 5 — what to check if it fails
- Status 401 / 404: webhook URL is wrong or expired. Regenerate in Discord.
- Status -1 (HTTPClient local error): TLS handshake failed. Common on ESP8266 with cert checking — confirm
setInsecure()is called. - Connection drops mid-test: too many messages too fast — Discord rate-limits at 30/min per webhook.
- Reset loop: out of RAM. Move strings to
F("...")macros or upgrade to ESP32.
Step 6 — bonus: include a sensor reading
Read the LDR and include it in the message:
int raw = analogRead(A0);
String msg = "Doorbell at ";
msg += buf;
msg += " (light = ";
msg += raw;
msg += ")";Now Discord tells you not only that someone's at the door but how bright it is outside — useful for "at dusk, turn on the porch light too" automation.
Try It Yourself 15 min
Goal: Add a cooldown: don't send more than one message per 10 seconds, even if the button is mashed.
Hint
unsigned long lastSentAt = 0;
const unsigned long COOLDOWN_MS = 10000;
void onPress() {
if (millis() - lastSentAt < COOLDOWN_MS) {
Serial.println("# cooldown");
return;
}
// ... send ...
lastSentAt = millis();
}Spam protection — both for the user's Discord channel and for the rate limit.
Goal: Replace the button with a sensor threshold. If the LDR drops below 200 (i.e. it's dark), POST "# It got dark at HH:MM:SS". Once-per-event, with hysteresis (don't re-fire until brightness goes > 400 again).
Hint
State variable bool isDark. On each loop: if raw < 200 && !isDark → fire "got dark", set isDark = true. If raw > 400 && isDark → fire "got bright", set isDark = false. The gap between 200 and 400 is the hysteresis band — prevents flickering at twilight.
Goal: Make the webhook URL configurable via your tiny web server. POST a new URL to /config/webhook; save to EEPROM. The Discord URL is no longer hardcoded in the sketch.
Hint
Reserve EEPROM bytes 8..119 for the URL string (Discord URLs are ~110 chars). On boot, read into a buffer; if blank, use a fallback. On POST, validate the URL starts with https://discord.com/api/webhooks/ before saving.
This is the "configurable IoT device" pattern. Real smart devices use the same idea but with a captive portal at first boot — we'll do that in L04.
Mini-Challenge · Wire one webhook into your existing project 10 min
Take one of your earlier projects and add a webhook trigger.
- Mood lamp (L01-35): POST "mood changed" whenever the user changes the colour preset.
- Smart bin (L02-26): POST "bin opened" on every lid opening.
- Smart lamp (L03-34): POST "lamp turned on / off" on every state change.
- Sensor dashboard (L03-33): POST when the sensor crosses a threshold.
One webhook line of code transforms a local IoT toy into a connected device. Your phone gets a Discord notification every time your project does something interesting. Magic for the first 24 hours; turn off after.
Recap 5 min
A webhook is an outbound HTTP POST your device fires when something happens. Discord, IFTTT, Slack, and your own server all expose webhook URLs you can POST JSON to. On ESP, the path is SecureClient + setInsecure() + HTTPClient + POST(body). Always cooldown / rate-limit; always keep the URL secret. Combine with sensors and the door is open to real IoT — your fridge can DM you when it's warm, your plant can email you when it's dry. Tomorrow we wrap Cluster G with a polished IFTTT-and-Discord lesson.
- Webhook
- An HTTP POST your device sends to a URL the receiving service has exposed for triggers. The reverse of polling.
- HTTPS
- HTTP over TLS. Required by most modern webhook providers (Discord, Slack, IFTTT).
- WiFiClientSecure
- The TLS-capable client class. Use it instead of
WiFiClientfor HTTPS URLs.setInsecure()disables certificate verification (OK for school). - POST with body
- The HTTP verb for "send data to be processed". Carries a body in the request.
http.POST(jsonBody)on Arduino. - Content-Type
- HTTP header that tells the receiver how to parse the body.
application/jsonfor JSON payloads. - Rate limit
- The cap on how many messages a service accepts per unit time per source. Discord: 30 per minute per webhook. Always respect.
- Cooldown
- Client-side rate limit — don't re-send for N seconds after a successful POST. Cheap insurance against runaway loops.
- Hysteresis
- A "gap" in trigger thresholds to prevent flickering. E.g. fire "dark" below 200, "light" above 400; nothing fires in 201..399.
Homework 5 min
- Set up a Discord webhook for yourself (or IFTTT if you don't use Discord). Save the URL securely.
- Get the doorbell sketch working. Screenshot the Discord message that appeared after a button press.
- Add a webhook to one of your earlier projects (Mini-Challenge §6).
- Read ahead to ARD-L03-37 (IFTTT and Discord). Tomorrow we polish — multiple destinations, rich embeds, your first real automation recipe.
Bring back next class:
- Working doorbell + Discord screenshot.
- Your webhook-added earlier project + screenshot.