SMS OTP Verification in Python: Automate with Virtual Numbers
Manually entering OTP codes during development gets old fast. Whether you're running integration tests, automating account setups, or building a verification bypass for your own service, automating SMS OTP collection with Python turns a manual 2-minute process into a 10-second script.
This tutorial walks through a complete, production-ready SMS OTP collection system using the NumberOTP API — covering number acquisition, SMS polling, OTP extraction, and error handling.
Prerequisites
- Python 3.8+
-
requestslibrary (pip install requests) - A NumberOTP API key (free $0.10 credits on signup)
Architecture Overview
The flow has three stages:
1. Request a virtual number (country + service specific)
↓
2. Use that number for SMS verification on the target platform
↓
3. Poll the NumberOTP API for incoming SMS → extract OTP
The number is rented for a window (typically 20 minutes). If no SMS arrives, credits are auto-refunded.
Part 1: Setting Up the Client
import requests
import time
import re
import logging
from typing import Optional
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
API_KEY = "your_numberotp_api_key_here"
BASE_URL = "https://numberotp.com/api"
class NumberOTPClient:
def __init__(self, api_key: str):
self.api_key = api_key
self.session = requests.Session()
self.session.headers.update({"X-API-Key": api_key})
def get_number(self, country: str = "us", service: str = "google") -> dict:
response = self.session.get(
f"{BASE_URL}/numbers",
params={"country": country, "service": service}
)
response.raise_for_status()
data = response.json()
logger.info(f"Got number: {data['number']} (ID: {data['id']})")
return data
def get_sms(self, number_id: str) -> list:
response = self.session.get(f"{BASE_URL}/sms/{number_id}")
response.raise_for_status()
return response.json().get("messages", [])
def cancel_number(self, number_id: str) -> bool:
response = self.session.delete(f"{BASE_URL}/numbers/{number_id}")
return response.status_code == 200
Part 2: SMS Polling with Exponential Backoff
def wait_for_sms(
client: NumberOTPClient,
number_id: str,
timeout: int = 120,
initial_interval: float = 3.0,
max_interval: float = 15.0
) -> Optional[str]:
deadline = time.monotonic() + timeout
interval = initial_interval
attempt = 0
while time.monotonic() < deadline:
attempt += 1
messages = client.get_sms(number_id)
if messages:
message = messages[0]["body"]
logger.info(f"SMS received on attempt {attempt}")
return message
remaining = deadline - time.monotonic()
sleep_time = min(interval, remaining)
if sleep_time <= 0:
break
time.sleep(sleep_time)
interval = min(interval * 1.5, max_interval)
logger.warning(f"Timeout after {attempt} attempts")
return None
Part 3: OTP Extraction
OTP formats vary by platform — 4 to 8 digit codes, sometimes with dashes. This handles the common patterns:
def extract_otp(message: str) -> Optional[str]:
patterns = [
r'(?:code|otp|pin|verification)[\:\s]+([A-Z0-9]{4,8})',
r'\b(\d{6})\b', # 6-digit numeric (most common)
r'\b(\d{4})\b', # 4-digit PIN
r'\b(\d{5,8})\b', # 5-8 digit
]
for pattern in patterns:
match = re.search(pattern, message, re.IGNORECASE)
if match:
return match.group(1)
return None
Part 4: Complete End-to-End Function
def get_sms_otp(
country: str = "us",
service: str = "google",
timeout: int = 120
) -> Optional[str]:
client = NumberOTPClient(API_KEY)
number = None
try:
number = client.get_number(country=country, service=service)
print(f"\nUse this number: {number['number']}")
print("Waiting for SMS...\n")
message = wait_for_sms(client, number["id"], timeout=timeout)
if not message:
logger.warning("No SMS received, cancelling number for refund")
client.cancel_number(number["id"])
return None
otp = extract_otp(message)
logger.info(f"Extracted OTP: {otp}")
return otp
except Exception as e:
logger.error(f"Error: {e}")
if number:
client.cancel_number(number["id"])
raise
# Usage
otp = get_sms_otp(country="us", service="google", timeout=120)
if otp:
print(f"OTP: {otp}")
else:
print("No OTP received (credits refunded)")
Service Codes Reference
Common service parameter values for the NumberOTP API:
| Platform | Service Code |
|---|---|
google |
|
whatsapp |
|
| Telegram | telegram |
| Discord | discord |
| Amazon | amazon |
| Twitter/X | twitter |
instagram |
|
| Any platform | any |
Integration with Playwright
Combining with browser automation:
from playwright.sync_api import sync_playwright
def automated_verification():
client = NumberOTPClient(API_KEY)
number = client.get_number(country="us", service="google")
with sync_playwright() as p:
browser = p.chromium.launch()
page = browser.new_page()
# Navigate to your verification page
page.goto("https://example.com/verify")
page.fill('[name="phone"]', number["number"])
page.click('[type="submit"]')
# Wait for SMS
message = wait_for_sms(client, number["id"])
otp = extract_otp(message)
# Enter the OTP
page.fill('[name="code"]', otp)
page.click('[type="submit"]')
browser.close()
return otp
Frequently Asked Questions
What Python library works best for SMS OTP automation?
The requests library is sufficient for polling the NumberOTP API. For full automation including browser interaction, combine it with playwright or selenium. No specialized SMS library is needed when using a REST API like NumberOTP.
How do I handle rate limits when polling for SMS?
Use exponential backoff as shown in Part 2. Start at 3-second intervals and back off up to 15 seconds. Most OTPs arrive within 30 seconds of the SMS being sent.
Can I receive multiple SMS to the same virtual number?
Yes. The NumberOTP API supports receiving multiple messages to one number ID. The messages array contains all received SMS in order — useful for testing re-send flows.
What happens if the OTP SMS never arrives?
Cancel the number via the API (DELETE /numbers/{id}) to trigger auto-refund. Build this into your error handling as shown in the get_sms_otp function.
Is it legal to use virtual numbers for SMS verification?
Using virtual numbers for your own testing, development, and legitimate account management is legal. Creating fake accounts to defraud services or circumvent bans may violate terms of service and local laws. Always use virtual numbers responsibly.
Conclusion
SMS OTP automation in Python is straightforward once you have a reliable virtual number API. Get a SIM-backed non-VoIP number, poll for SMS with backoff, and extract the OTP with a robust regex pattern.
The NumberOTP API is the cleanest option for this workflow — real carrier numbers, auto-refund on failure, and a simple REST interface. Get your API key →
Top comments (0)