📡 Python Hands‑On: Auto‑Receive SMS Verification Codes with Flask & SIM800L
A practical guide by a tinkerer who hates reaching for the phone when signing up for services. We'll turn a $5 GSM module into your personal SMS API.
Why I Built This — The “Stop Looking at Your Phone” Moment
Last month I had to register a dozen test accounts for a side project. Every single one wanted a phone number. I was tired of picking up my personal phone, squinting at verification codes, then typing them into my browser. I thought: “Can't a script do this for me?”
So I dug out a SIM800L module from my junk box, a spare SIM card, and wrote a little Flask API that listens for incoming SMS and serves the latest code on demand. That’s what we’re building today. Strap in — we’ll go from wiring to running code, with every embarrassing mistake I made along the way documented for your entertainment.
Choosing Your Poison: Three Ways to Auto‑Receive SMS
Before we start soldering, let’s be honest about the options:
| Approach | How it works | Pros | Cons |
|---|---|---|---|
| A. Cloud Webhook (Twilio etc.) | Buy a virtual number, set a webhook URL, receive SMS via POST. | No hardware, scalable | Costs per message, limited local numbers, may not receive Chinese OTP codes |
| B. Android phone + SMS Forwarder app | Install an app on an old Android phone that forwards SMS to a URL. | Cheap (if you have a spare phone) | Phone must stay on and connected; battery bloat risk; app privacy concerns |
| C. SIM800L / 4G module + Python | Use a bare GSM module controlled via AT commands over UART. | Total control, one‑time cost (~$10), huge learning value | Needs wiring, power supply care, and handling of raw AT commands |
I chose Plan C. It’s the most educational and gives you a permanent, independent number that works as long as you have a prepaid SIM card. Let’s build it.
🛠️ Hardware Shopping List & Wiring
What you need
- SIM800L module (the purple board, v2.0, with antenna connector)
- GSM antenna (a small pigtail or PCB antenna — don’t skip this, or signal is terrible)
- USB‑to‑TTL converter (CP2102 or CH340, 3.3V logic)
- Micro SIM card (activated, with SMS credit)
- Power supply — critical! 5V 2A minimum. I used a separate 5V/2.5A wall adapter. The Raspberry Pi’s USB port often cannot supply the 2A peak current the SIM800L needs during transmission.
- Jumper wires (female‑to‑female)
Wiring diagram (text description)
Important: The SIM800L is a 3.3V logic device, but its VCC can take 5V. The USB‑TTL converter must be set to 3.3V mode (look for a jumper) — if you use 5V logic, you might damage the module. I learned that the smelly way.
First power‑up & AT command test
Before writing a single line of Python, we verify the module works manually. On Linux/Mac:
# Install miniterm if needed
pip install pyserial
# Connect to serial port (find yours with ls /dev/tty*)
python -m serial.tools.miniterm /dev/ttyUSB0 115200
Once connected, type AT and hit Enter. You should see OK. Then try:
AT+CMGF=1 # set text mode (easier to parse)
AT+CNMI=2,2,0,0,0 # enable new message indication via UART
If you see garbage characters, your baud rate is probably wrong. The SIM800L defaults to auto‑baud, but many modules ship with 9600 or 115200. Try both. If the module resets when you send a command, your power supply is too weak — the voltage dips during transmission and the board browns out. Use a proper 2A adapter.
💻 Software Setup: Python Environment
We need only a couple of libraries. Create a virtual environment and install:
pip install flask pyserial
The code runs on any machine with a USB port — a Raspberry Pi, an old laptop, or even a desktop. I used a Raspberry Pi 4 for low power consumption.
🧠 The Architecture in One ASCII Graph
Incoming SMS → parsed in a background thread → pushed into a queue → Flask serves to clients.
📜 Core Code Walkthrough
We'll split the project into two files: serial_reader.py (background serial listener) and app.py (Flask API). Both share a global message_queue.
1. serial_reader.py – The Serial Listener Thread
import serial
import time
import re
import threading
import queue
# Global queue to pass messages to Flask
message_queue = queue.Queue(maxsize=100)
class SMSReader(threading.Thread):
def __init__(self, port, baudrate=115200, timeout=1):
super().__init__(daemon=True)
self.ser = serial.Serial(port, baudrate, timeout=timeout)
self.buffer = ""
def send_at(self, cmd, wait=0.5):
self.ser.write((cmd + '\r\n').encode())
time.sleep(wait)
def run(self):
# Initialize module
self.send_at('AT')
self.send_at('AT+CMGF=1') # text mode
self.send_at('AT+CNMI=2,2,0,0,0') # auto new message notification
self.send_at('AT+CMGD=1,4') # delete all stored SMS to avoid memory full
while True:
try:
if self.ser.in_waiting:
chunk = self.ser.read(self.ser.in_waiting).decode('utf-8', errors='ignore')
self.buffer += chunk
# Process lines
while '\n' in self.buffer:
line, self.buffer = self.buffer.split('\n', 1)
line = line.strip()
if line.startswith('+CMT:') or line.startswith('+CMTI:'):
# New SMS indication
self.handle_new_sms()
except Exception as e:
print(f"Serial error: {e}")
time.sleep(2)
def handle_new_sms(self):
# Read the actual message using AT+CMGR=
# Since we set CNMI=2,2, the message lines follow +CMT header directly
# In text mode, we get: +CMT: "sender",,"date/time" then next line is body
self.send_at('AT+CMGR=1') # read latest (or we can parse index from CMTI)
time.sleep(0.8)
# Read response
if self.ser.in_waiting:
raw = self.ser.read(self.ser.in_waiting).decode('utf-8', errors='ignore')
# Extract message body between quotes and content after newline
match = re.search(r'\+CMGR:.*?"(.*?)".*?\n(.*)', raw, re.DOTALL)
if match:
sender = match.group(1)
body = match.group(2).strip()
# Try to decode UCS-2 if it looks like hex
if body and all(c in '0123456789ABCDEFabcdef ' for c in body):
try:
hex_str = body.replace(' ', '')
body = bytes.fromhex(hex_str).decode('utf-16-be')
except:
pass
print(f"📩 SMS from {sender}: {body}")
if not message_queue.full():
message_queue.put({'sender': sender, 'text': body, 'time': time.time()})
# Delete message to keep SIM clean
self.send_at('AT+CMGD=1')
def start_reader(port):
reader = SMSReader(port)
reader.start()
return reader
2. app.py – The Flask API
from flask import Flask, jsonify, request, render_template_string
import time
from serial_reader import message_queue, start_reader
app = Flask(__name__)
# Simple HTML page for real‑time viewing
HTML_PAGE = '''
<!DOCTYPE html>
<html>
<head><title>SMS Inbox</title></head>
<body>
<h1>📨 Latest SMS</h1>
<pre id="msg">Loading...</pre>
<script>
async function fetchMsg() {
const res = await fetch('/sms/latest');
const data = await res.json();
if (data.success) {
document.getElementById('msg').textContent = JSON.stringify(data.message, null, 2);
}
}
setInterval(fetchMsg, 2000);
fetchMsg();
</script>
</body>
</html>
'''
@app.route('/')
def index():
return HTML_PAGE
@app.route('/sms/latest', methods=['GET'])
def latest_sms():
msgs = []
while not message_queue.empty():
try:
msgs.append(message_queue.get_nowait())
except:
break
if not msgs:
return jsonify({'success': False, 'message': None, 'hint': 'No new SMS'})
latest = msgs[-1]
# Try to extract verification code (4‑6 digits)
code_match = re.search(r'\b(\d{4,6})\b', latest['text'])
latest['code'] = code_match.group(1) if code_match else None
return jsonify({'success': True, 'message': latest})
@app.route('/sms/clear', methods=['POST'])
def clear_sms():
while not message_queue.empty():
try:
message_queue.get_nowait()
except:
break
return jsonify({'status': 'cleared'})
if __name__ == '__main__':
import argparse, re
parser = argparse.ArgumentParser()
parser.add_argument('--port', default='/dev/ttyUSB0', help='Serial port')
args = parser.parse_args()
start_reader(args.port)
app.run(host='0.0.0.0', port=5000, debug=False)
3. The Verification Code Extractor
Inside the /sms/latest endpoint, we use a simple regex r'\b(\d{4,6})\b' to grab the first 4‑to‑6‑digit number. This works for most OTP messages but be careful: if the message contains other numbers like phone numbers, you might need to tune the pattern (e.g., look for “code”, “验证码”, or a numeric sequence preceded by specific keywords).
🖥️ Running the Whole Thing
Fire up the app:
python app.py --port /dev/ttyUSB0
Now call your SIM800L’s number from another phone, hang up, and then send an SMS to it. Within seconds you should see the message printed in the terminal, and hitting http://localhost:5000/sms/latest returns JSON with the extracted code.
💣 Four Rabbit Holes I Fell Into (So You Don’t Have To)
Pitfall #1: Power Starvation = Spontaneous Reboots
When the SIM800L transmits, it can draw up to 2A in short bursts. A typical Raspberry Pi USB port provides only 1.2A max. Result: the module kept resetting every time it tried to register on the network. The fix was an external 5V 2.5A power adapter with the power wires directly soldered to the module pins. Never power it from a computer USB port for production use.
Pitfall #2: Baud Rate Mismatch & Garbage
I spent an hour staring at ÿÿ¿½¿½ in my terminal. The SIM800L defaults to auto‑baud, but if you send AT at the wrong speed initially, it may lock onto a wrong rate. Always start with 115200; if that fails, try 9600. You can also send AT+IPR=115200 to permanently fix it.
Pitfall #3: Chinese SMS Turned into Hex Gibberish
When receiving a message like “您的验证码是123456”, the SIM800L in text mode sometimes delivers the body as a UCS‑2 hex string like 60A8 7684 9A8C 8BC1.... I had to detect hex and decode with bytes.fromhex(hex_str).decode('utf-16-be'). That snippet is already in the code above — you’re welcome.
Pitfall #4: SMS Flood & Queue Overflow
One time a service bombarded my test number with 20 verification codes in a minute. The serial thread couldn't keep up and the message buffer in the module filled up, causing older messages to be deleted. I added a maxsize=100 queue and a check for queue.full() to avoid blocking. For production, you’d want a persistent store like Redis.
🔧 Going Further: Ideas for Your New SMS API
- Voice announcement: Use
espeakor a TTS service to read codes aloud on your desktop. - WeChat/Telegram notification: Make the Flask API forward new SMS to a bot webhook.
- Multi‑modem pool: Connect several SIM800L modules via a USB hub for higher concurrency.
- WebSocket push: Replace polling with Flask‑SocketIO for real‑time updates.
A final word on ethics: This project is for educational and personal convenience. Using it to bypass service rate limits, commit fraud, or violate Terms of Service can land you in hot water. Be responsible.
🏁 Wrap‑Up
You now have a $10 hardware device that acts as your personal SMS gateway. You’ve wrestled with AT commands, serial buffering, and power circuits — and you have a working Flask API to show for it. The next time a verification code arrives, your script can catch it before you even reach for your phone. That’s the kind of automation that makes tinkering worth it.
Happy building, and may your power supply always be beefy enough.