A Selenium suite that stops at "enter the code we emailed you" either mocks the mail step or shares one hand-typed inbox across every test run — both are fragile. OTPBox gives a Python + Selenium test a real disposable address, a real inbox to poll, and a way to delete it when the test is done, so the OTP screen gets exercised the same way a real user hits it.
Every WebDriver flow below is the same four steps: mint a key and create a real inbox, type its address into the signup form Selenium is driving, poll the inbox until the code shows up, then submit it and tear the inbox down. There's no first-party otpbox Python package documented, so the REST calls below use the plain requests library — it's a handful of lines and keeps the dependency list short.
Mint a free key once (200 requests/month — see Get a key) and store it as OTPBOX_KEY in your environment. A small pytest fixture keeps the inbox lifecycle out of the test body:
import os
import time
import requests
import pytest
from selenium import webdriver
from selenium.webdriver.common.by import By
OTPBOX_KEY = os.environ["OTPBOX_KEY"]
BASE = "https://otpbox.io/api/v1"
HEADERS = {"authorization": f"Bearer {OTPBOX_KEY}", "content-type": "application/json"}
@pytest.fixture
def inbox():
res = requests.post(f"{BASE}/inboxes", headers=HEADERS, json={})
res.raise_for_status()
box = res.json() # {id, address, domain, expiresAt, token}
try:
yield box
finally:
requests.delete(f"{BASE}/inboxes/{box['id']}", headers=HEADERS)
@pytest.fixture
def driver():
d = webdriver.Chrome()
yield d
d.quit()
The inbox fixture's finally block runs whether the test passes or fails, so a failed assertion never leaves the inbox orphaned — though the 15-minute expiry cron would clean it up either way.
Poll GET /api/v1/inboxes/:id/messages until a message has an extracted code. Selenium has no built-in async wait for an out-of-band event like this, so a small retry loop does the job (Selenium's own WebDriverWait is for DOM conditions, not for polling an external API):
def wait_for_otp(inbox_id, timeout_s=20, interval_s=1.5):
deadline = time.monotonic() + timeout_s
while time.monotonic() < deadline:
res = requests.get(f"{BASE}/inboxes/{inbox_id}/messages", headers=HEADERS)
res.raise_for_status()
messages = res.json()["messages"]
if messages and messages[0].get("code"):
return messages[0]["code"]
time.sleep(interval_s)
return None
def test_signup_with_real_otp(driver, inbox):
driver.get("https://your-app.example.com/signup")
driver.find_element(By.NAME, "email").send_keys(inbox["address"])
driver.find_element(By.CSS_SELECTOR, "button[type='submit']").click()
code = wait_for_otp(inbox["id"])
assert code is not None, "no OTP arrived before the timeout"
driver.find_element(By.NAME, "otp").send_keys(code)
driver.find_element(By.CSS_SELECTOR, "button[type='submit']").click()
assert "Welcome" in driver.find_element(By.TAG_NAME, "body").text
The shape is identical — only the HTTP client and assertion library change. Use java.net.http.HttpClient (built in since Java 11) for the REST calls and drive the same polling loop from an @AfterEach-style teardown:
HttpClient http = HttpClient.newHttpClient();
HttpRequest createInbox = HttpRequest.newBuilder()
.uri(URI.create("https://otpbox.io/api/v1/inboxes"))
.header("authorization", "Bearer " + otpboxKey)
.header("content-type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString("{}"))
.build();
HttpResponse<String> res = http.send(createInbox, HttpResponse.BodyHandlers.ofString());
// parse res.body() for id/address/token, then drive WebDriver and poll
// GET /api/v1/inboxes/{id}/messages the same way, reading .messages[0].code
The retry loop above is simple and fine for a local suite, but if your test runner already accepts inbound HTTP (for example a CI job with a reachable endpoint), you can register a webhook on otp.extracted instead of polling — see Webhooks. For most Selenium suites running against a browser anyway, a short poll loop like wait_for_otp above is simpler to reason about and doesn't require exposing anything.
OTPBOX_KEY as a CI secret, never hardcoded in the test file or checked in — see CI/CD for GitHub Actions, GitLab CI, Jenkins and CircleCI examples (the same secret-injection pattern applies regardless of language).Idempotency-Key header on POST /api/v1/inboxes if your grid retries flaky setup steps, so a retry doesn't create a second inbox — see Idempotency keys.finally/teardown block, as in the pytest fixture above, so a failed run doesn't leave orphaned inboxes around.Ready to try it against your own app? Create a free account
← Back to OTPBox