You unbox a small IoT device, plug it in, and… nothing.
No screen. No keyboard. No buttons that make any sense.
This is a familiar situation if you've ever worked with microcontrollers like the ESP32. The hardware is powerful, but the very first interaction is often awkward. At some point, the device needs to connect to your Wi-Fi network, and the classic question appears:
"How does a tiny device ask for Wi-Fi credentials?"
Hardcoding credentials works for quick experiments, but it breaks down in the real world, every deployment would require re-flashing the firmware. Serial consoles are fine for developers, not for users who don't have USB cables or the patience for terminal commands. Mobile apps add friction and maintenance overhead, requiring separate iOS and Android versions that need constant updates.
What you want instead is simple: power on the device, connect to it once from any phone or laptop, configure it through a familiar web interface, and never think about it again. No apps to install, no cables to find, no special tools required.
This is exactly where captive portals shine, they turn Wi-Fi provisioning into a one-time, friction-free experience that works on every platform.
What Is a Captive Portal?
If you've ever connected to Wi-Fi at a coffee shop, hotel, or airport, you've used a captive portal.
You join the network, and a web page automatically pops up asking you to log in or accept terms.
You didn't type a URL—the network guided you there by intercepting your browser traffic.
A captive portal on an embedded device works the same way:
The beauty of this approach is that the ESP32 can handle everything on-device.
It acts as both an access point and a web server simultaneously, with no cloud services, external infrastructure, or internet connection required during setup. It's significantly simpler than building a companion mobile app (which requires maintaining iOS and Android versions) and far more user-friendly than hardcoded credentials or serial console configuration.
For IoT products, this is often the difference between "technically works" and "actually ships to customers."
Architecture Overview
The device goes through two distinct phases: setup mode (captive portal) and normal operation (Wi-Fi client).
Setup Mode runs the first time you power on the device, or anytime no Wi-Fi configuration exists.
In this mode, the ESP32:
Normal Operation begins after successful configuration.
In this mode, the ESP32:
The key decision happens at boot: does config.json exist? If yes, connect to Wi-Fi. If no, start the captive portal.

If you have already MicroPython installed on your ESP32, you are good to go and experiment with the following examples.
If not, see this guide for more detail on how to setup an ESP32 with MicroPython.
Step 1 - Create the Wi-Fi Access Point
The ESP32 creates its own Wi-Fi network using the built-in network module. This is the foundation of the entire captive portal, without the access point, users have no way to reach the configuration page.
The ESP32 has two Wi-Fi interfaces:
During setup mode, we use AP_IF to create a temporary network that users can join from their phones.
In your local project, create a new file wifi_ap.py:
import network
def start_access_point():
ap = network.WLAN(network.AP_IF)
ap.active(True)
ap.config(
essid="MyDevice-Setup",
authmode=network.AUTH_OPEN
)
print("Access point active:", ap.active())
print("AP IP address:", ap.ifconfig()[0])
return ap
Once this runs, the ESP32 broadcasts a Wi-Fi network named "MyDevice-Setup" and assigns itself IP address 192.168.4.1. Any device that connects to this network can communicate with the ESP32 using this IP.
Why use an open network?
You'll notice we use AUTH_OPEN (no password). This might seem insecure, but it's actually the right choice for a temporary setup network:
The security risk is minimal because:
For production devices, you can add strategies like auto-disabling the AP after 10 minutes, or requiring a physical button press to re-enter setup mode.
Step 2 - Build the HTML Setup Page
Before we create the HTTP server, let's build the setup page that users will see. This is the critical user-facing component, it needs to work flawlessly on every mobile device.
Design Principles for Captive Portal Pages:
The page needs only two inputs: SSID and password. Everything else can be configured later through a different interface (if needed at all).
On your local project folder, create portal.html:
name="viewport" content="width=device-width, initial-scale=1">
</span>Device Setup<span class="nt">
Wi-Fi Setup
method="POST" action="/save">
Wi-Fi SSID
name="ssid" required>
Password
name="password" type="password">
type="submit">Save & Connect
No screen. No keyboard. No buttons that make any sense.
This is a familiar situation if you've ever worked with microcontrollers like the ESP32. The hardware is powerful, but the very first interaction is often awkward. At some point, the device needs to connect to your Wi-Fi network, and the classic question appears:
"How does a tiny device ask for Wi-Fi credentials?"
Hardcoding credentials works for quick experiments, but it breaks down in the real world, every deployment would require re-flashing the firmware. Serial consoles are fine for developers, not for users who don't have USB cables or the patience for terminal commands. Mobile apps add friction and maintenance overhead, requiring separate iOS and Android versions that need constant updates.
What you want instead is simple: power on the device, connect to it once from any phone or laptop, configure it through a familiar web interface, and never think about it again. No apps to install, no cables to find, no special tools required.
This is exactly where captive portals shine, they turn Wi-Fi provisioning into a one-time, friction-free experience that works on every platform.
What Is a Captive Portal?
If you've ever connected to Wi-Fi at a coffee shop, hotel, or airport, you've used a captive portal.
You join the network, and a web page automatically pops up asking you to log in or accept terms.
You didn't type a URL—the network guided you there by intercepting your browser traffic.
A captive portal on an embedded device works the same way:
- The device creates its own Wi-Fi network (acting as an access point)
- The user connects from their phone or laptop (no password needed)
- The device intercepts all internet requests using a DNS server
- A web page automatically opens showing the configuration form
- After submission, the device saves credentials and switches to normal Wi-Fi client mode
The beauty of this approach is that the ESP32 can handle everything on-device.
It acts as both an access point and a web server simultaneously, with no cloud services, external infrastructure, or internet connection required during setup. It's significantly simpler than building a companion mobile app (which requires maintaining iOS and Android versions) and far more user-friendly than hardcoded credentials or serial console configuration.
For IoT products, this is often the difference between "technically works" and "actually ships to customers."
Architecture Overview
The device goes through two distinct phases: setup mode (captive portal) and normal operation (Wi-Fi client).
Setup Mode runs the first time you power on the device, or anytime no Wi-Fi configuration exists.
In this mode, the ESP32:
- Creates its own temporary Wi-Fi network
- Runs a DNS server that redirects all domains to itself
- Runs an HTTP server that serves the configuration page
- Saves user-submitted credentials to flash storage
Normal Operation begins after successful configuration.
In this mode, the ESP32:
- Disables the access point
- Connects to the user's Wi-Fi network as a client
- Runs your actual application (fetching data, controlling hardware, etc.)
The key decision happens at boot: does config.json exist? If yes, connect to Wi-Fi. If no, start the captive portal.

If you have already MicroPython installed on your ESP32, you are good to go and experiment with the following examples.
If not, see this guide for more detail on how to setup an ESP32 with MicroPython.
Step 1 - Create the Wi-Fi Access Point
The ESP32 creates its own Wi-Fi network using the built-in network module. This is the foundation of the entire captive portal, without the access point, users have no way to reach the configuration page.
The ESP32 has two Wi-Fi interfaces:
- AP_IF (Access Point Interface) - Makes the ESP32 act like a router
- STA_IF (Station Interface) - Makes the ESP32 act like a Wi-Fi client
During setup mode, we use AP_IF to create a temporary network that users can join from their phones.
In your local project, create a new file wifi_ap.py:
import network
def start_access_point():
ap = network.WLAN(network.AP_IF)
ap.active(True)
ap.config(
essid="MyDevice-Setup",
authmode=network.AUTH_OPEN
)
print("Access point active:", ap.active())
print("AP IP address:", ap.ifconfig()[0])
return ap
Once this runs, the ESP32 broadcasts a Wi-Fi network named "MyDevice-Setup" and assigns itself IP address 192.168.4.1. Any device that connects to this network can communicate with the ESP32 using this IP.
Why use an open network?
You'll notice we use AUTH_OPEN (no password). This might seem insecure, but it's actually the right choice for a temporary setup network:
- Better compatibility: Some devices have issues with WPA2 on captive portals
- Faster setup: Users don't need to type a password
- Auto-detection works better: Operating systems are more likely to show the "Sign in to network" popup on open networks
- Temporary by design: The AP only exists during initial setup and disappears once configured
The security risk is minimal because:
- The network only exists during the 1-2 minutes of setup
- The user is typically standing right next to the device
- Once configured, the AP shuts down completely
For production devices, you can add strategies like auto-disabling the AP after 10 minutes, or requiring a physical button press to re-enter setup mode.
Step 2 - Build the HTML Setup Page
Before we create the HTTP server, let's build the setup page that users will see. This is the critical user-facing component, it needs to work flawlessly on every mobile device.
Design Principles for Captive Portal Pages:
- Mobile-first: Most users will access this from their phones
- Minimal dependencies: No external CSS/JS frameworks (we don't have internet!)
- Fast loading: Every byte travels over a slow access point connection
- Clear purpose: Users should immediately understand what to do
- Forgiving inputs: Don't assume users type perfectly
The page needs only two inputs: SSID and password. Everything else can be configured later through a different interface (if needed at all).
On your local project folder, create portal.html:
name="viewport" content="width=device-width, initial-scale=1">
</span>Device Setup<span class="nt">
Wi-Fi Setup
method="POST" action="/save">
Wi-Fi SSID
name="ssid" required>
Password
name="password" type="password">
type="submit">Save & Connect