Temp Mail API for Selenium: Automate Email Verification in Your Test Suite (2026)

Email verification is the wall that Selenium test suites hit every time. You can automate the browser perfectly — form fill, submit, redirect — but the moment your app sends a verification email, you need a real, readable inbox to continue. Most teams solve this badly: a shared Gmail account, a hardcoded test address that everyone uses, or worse, skipping the email step entirely and calling it "tested."

This guide shows you how to do it properly using FreeCustom.Email, the only disposable email provider with an official CLI, official SDKs, real-time WebSocket delivery, and an OTP extraction endpoint built specifically for this use case. No regex. No shared state. No flakiness.


Why Shared Inboxes Destroy Selenium Tests

Before writing any code, it is worth understanding exactly why the common approaches fail at scale:

Approach

Why it breaks in Selenium

Shared Gmail account

Parallel tests receive each other's OTPs; wrong code entered; test fails non-deterministically

Hardcoded temp mail URL

No API access; requires a second browser session to fetch the email

nodemailer SMTP client

Requires credential management; OAuth rotation; no OTP parsing

Mocking email in the app

Does not test the actual email delivery pipeline; misses real sending failures

Polling a basic REST API

Slow; rate-limited; requires regex; breaks when email format changes

Manual test execution

Scales to zero; dies in CI

The correct solution is fresh isolated inboxes per test, real-time delivery, and automatic OTP extraction. That is exactly what the FreeCustom.Email API provides.


Architecture Overview

Here is the pattern that works reliably at any scale:

1. Before test: create a fresh disposable inbox via FCE API or CLI
2. Selenium: fill the signup form with that inbox address
3. FCE API: poll or wait for the OTP (< 200ms via WebSocket)
4. Selenium: fill the OTP field and submit
5. After test: clean up the inbox

Each test gets its own inbox. Parallel tests never interfere. The inbox is disposable so there is no cleanup debt.


Setup

Get an API key

Register at freecustom.email and run:

bash

fce login

This opens your browser, authenticates, and saves your API key to your OS keychain automatically. For CI, grab the key from the dashboard and set it as FCE_API_KEY.

Install the CLI for local use and script-based approaches:

bash

# macOS / Linux
curl -fsSL freecustom.email/install.sh | sh

# Homebrew
brew tap DishIs/homebrew-tap && brew install fce

# npm
npm install -g fcemail@latest

# Windows (Scoop)
scoop bucket add fce https://github.com/DishIs/scoop-bucket && scoop install fce

# Windows (Chocolatey)
choco install fce

Latest releases: github.com/DishIs/fce-cli/releases

For SDK-based tests, install the language package:

bash

npm install freecustom-email        # Node.js
pip install freecustom-email        # Python

Python + Selenium + FCE API

This is the most common setup — Python Selenium with pytest. The FreeCustom.Email Python SDK handles inbox creation, waiting, and OTP extraction.

Install dependencies

bash

pip install selenium pytest pytest-asyncio freecustom-email

Test: complete signup verification flow

python

# tests/test_signup_selenium.py
import asyncio
import os
import pytest
import time
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from freecustom_email import FreeCustomEmail

fce = FreeCustomEmail(api_key=os.environ["FCE_API_KEY"])
APP_URL = os.environ.get("APP_URL", "https://staging.myapp.com")

@pytest.fixture
def driver():
    options = webdriver.ChromeOptions()
    options.add_argument("--headless")
    options.add_argument("--no-sandbox")
    options.add_argument("--disable-dev-shm-usage")
    drv = webdriver.Chrome(options=options)
    yield drv
    drv.quit()

@pytest.fixture
def inbox():
    """Fresh inbox per test — never reuse."""
    addr = f"selenium-{int(time.time())}@ditapi.info"
    asyncio.run(fce.inboxes.register(addr))
    yield addr
    asyncio.run(fce.inboxes.unregister(addr))

@pytest.mark.asyncio
async def test_signup_and_verify(driver, inbox):
    wait = WebDriverWait(driver, 15)

    # Step 1: Fill signup form with the temp inbox address
    driver.get(f"{APP_URL}/signup")
    wait.until(EC.presence_of_element_located((By.ID, "email"))).send_keys(inbox)
    driver.find_element(By.ID, "password").send_keys("TestPass123!")
    driver.find_element(By.CSS_SELECTOR, "button[type='submit']").click()

    # Step 2: Wait for verification screen
    wait.until(EC.presence_of_element_located((By.ID, "otp-input")))

    # Step 3: Get OTP — SDK handles waiting, no polling loop needed
    otp = await fce.otp.wait_for(inbox, timeout=30)
    assert otp is not None, f"No OTP received for {inbox}"
    assert otp.isdigit(), f"Unexpected OTP format: {otp}"

    # Step 4: Fill OTP and submit
    driver.find_element(By.ID, "otp-input").send_keys(otp)
    driver.find_element(By.CSS_SELECTOR, "button[type='submit']").click()

    # Step 5: Assert success
    wait.until(EC.url_contains("/dashboard"))
    assert "/dashboard" in driver.current_url, "Verification did not redirect to dashboard"

Parallel tests with pytest-xdist

Because each test creates its own inbox, parallel execution works out of the box:

bash

pip install pytest-xdist
pytest -n 4 tests/test_signup_selenium.py

Four browser sessions, four isolated inboxes, zero interference.


JavaScript + Selenium WebDriver + FCE SDK

For Node.js teams using selenium-webdriver:

bash

npm install selenium-webdriver freecustom-email

typescript

// tests/signup.test.ts
import { Builder, By, until } from 'selenium-webdriver';
import { FreecustomEmailClient } from 'freecustom-email';
import { describe, it, before, after } from 'mocha';
import assert from 'assert';

const fce = new FreecustomEmailClient({ apiKey: process.env.FCE_API_KEY! });
const APP_URL = process.env.APP_URL ?? 'https://staging.myapp.com';

describe('Signup verification flow', () => {
  let driver: any;
  let inbox: string;

  before(async () => {
    driver = await new Builder().forBrowser('chrome').build();
    inbox = `selenium-${Date.now()}@ditapi.info`;
    await fce.inboxes.register(inbox);
  });

  after(async () => {
    await driver.quit();
    await fce.inboxes.unregister(inbox);
  });

  it('completes signup and email verification', async () => {
    // Fill signup form
    await driver.get(`${APP_URL}/signup`);
    await driver.wait(until.elementLocated(By.id('email')), 10_000);
    await driver.findElement(By.id('email')).sendKeys(inbox);
    await driver.findElement(By.id('password')).sendKeys('TestPass123!');
    await driver.findElement(By.css("button[type='submit']")).click();

    // Wait for OTP screen
    await driver.wait(until.elementLocated(By.id('otp-input')), 10_000);

    // Extract OTP — no polling, no regex
    const otp = await fce.otp.waitFor(inbox, { timeout: 30_000 });
    assert.ok(otp, 'OTP not received');
    assert.match(otp, /^\d+$/, 'OTP is not numeric');

    // Submit OTP
    await driver.findElement(By.id('otp-input')).sendKeys(otp);
    await driver.findElement(By.css("button[type='submit']")).click();

    // Assert dashboard redirect
    await driver.wait(until.urlContains('/dashboard'), 10_000);
  });
});

Java + Selenium + FCE REST API

Java does not have an official SDK yet, so call the REST API directly with any HTTP client:

java

// src/test/java/SignupVerificationTest.java
import org.openqa.selenium.*;
import org.openqa.selenium.chrome.*;
import org.openqa.selenium.support.ui.*;
import org.junit.jupiter.api.*;
import java.net.http.*;
import java.net.URI;
import java.time.*;
import org.json.*;
import static org.junit.jupiter.api.Assertions.*;

public class SignupVerificationTest {
    private WebDriver driver;
    private String inbox;
    private static final String FCE_KEY = System.getenv("FCE_API_KEY");
    private static final String APP_URL = System.getenv("APP_URL");
    private static final String FCE_BASE = "https://api2.freecustom.email/v1";
    private static final HttpClient http = HttpClient.newHttpClient();

    @BeforeEach
    void setUp() throws Exception {
        ChromeOptions opts = new ChromeOptions();
        opts.addArguments("--headless", "--no-sandbox");
        driver = new ChromeDriver(opts);
        inbox = "selenium-" + System.currentTimeMillis() + "@ditapi.info";
        registerInbox(inbox);
    }

    @AfterEach
    void tearDown() throws Exception {
        if (driver != null) driver.quit();
        deleteInbox(inbox);
    }

    @Test
    void testSignupAndVerification() throws Exception {
        WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(15));

        // Fill signup form
        driver.get(APP_URL + "/signup");
        wait.until(ExpectedConditions.presenceOfElementLocated(By.id("email")))
            .sendKeys(inbox);
        driver.findElement(By.id("password")).sendKeys("TestPass123!");
        driver.findElement(By.cssSelector("button[type='submit']")).click();

        // Wait for OTP screen
        wait.until(ExpectedConditions.presenceOfElementLocated(By.id("otp-input")));

        // Poll FCE API for OTP (max 30 seconds)
        String otp = waitForOtp(inbox, 30);
        assertNotNull(otp, "OTP not received within timeout");
        assertTrue(otp.matches("\\d+"), "OTP is not numeric: " + otp);

        // Submit OTP
        driver.findElement(By.id("otp-input")).sendKeys(otp);
        driver.findElement(By.cssSelector("button[type='submit']")).click();

        // Assert dashboard redirect
        wait.until(ExpectedConditions.urlContains("/dashboard"));
        assertTrue(driver.getCurrentUrl().contains("/dashboard"));
    }

    private void registerInbox(String addr) throws Exception {
        var body = HttpRequest.BodyPublishers.ofString("{\"inbox\":\"" + addr + "\"}");
        var req = HttpRequest.newBuilder()
            .uri(URI.create(FCE_BASE + "/inboxes"))
            .header("Authorization", "Bearer " + FCE_KEY)
            .header("Content-Type", "application/json")
            .POST(body).build();
        http.send(req, HttpResponse.BodyHandlers.ofString());
    }

    private void deleteInbox(String addr) throws Exception {
        var req = HttpRequest.newBuilder()
            .uri(URI.create(FCE_BASE + "/inboxes/" + addr))
            .header("Authorization", "Bearer " + FCE_KEY)
            .DELETE().build();
        http.send(req, HttpResponse.BodyHandlers.ofString());
    }

    private String waitForOtp(String addr, int timeoutSecs) throws Exception {
        long deadline = System.currentTimeMillis() + timeoutSecs * 1000L;
        while (System.currentTimeMillis() < deadline) {
            var req = HttpRequest.newBuilder()
                .uri(URI.create(FCE_BASE + "/inboxes/" + addr + "/otp"))
                .header("Authorization", "Bearer " + FCE_KEY)
                .GET().build();
            var res = http.send(req, HttpResponse.BodyHandlers.ofString());
            if (res.statusCode() == 200) {
                var json = new JSONObject(res.body());
                if (json.optBoolean("success") && json.has("data")) {
                    return json.getJSONObject("data").optString("otp");
                }
            }
            Thread.sleep(2000);
        }
        return null;
    }
}

Password Reset Flow with Selenium

The same pattern works for password reset:

python

@pytest.mark.asyncio
async def test_password_reset(driver, inbox):
    wait = WebDriverWait(driver, 15)

    # First create and verify an account
    driver.get(f"{APP_URL}/signup")
    wait.until(EC.presence_of_element_located((By.ID, "email"))).send_keys(inbox)
    driver.find_element(By.ID, "password").send_keys("OriginalPass123!")
    driver.find_element(By.CSS_SELECTOR, "button[type='submit']").click()
    wait.until(EC.presence_of_element_located((By.ID, "otp-input")))

    signup_otp = await fce.otp.wait_for(inbox, timeout=30)
    driver.find_element(By.ID, "otp-input").send_keys(signup_otp)
    driver.find_element(By.CSS_SELECTOR, "button[type='submit']").click()
    wait.until(EC.url_contains("/dashboard"))

    # Now test password reset
    driver.get(f"{APP_URL}/forgot-password")
    wait.until(EC.presence_of_element_located((By.ID, "email"))).send_keys(inbox)
    driver.find_element(By.CSS_SELECTOR, "button[type='submit']").click()
    wait.until(EC.presence_of_element_located((By.ID, "reset-otp")))

    # Same inbox, new OTP — fce.otp.wait_for always gets the latest
    reset_otp = await fce.otp.wait_for(inbox, timeout=30)
    driver.find_element(By.ID, "reset-otp").send_keys(reset_otp)
    driver.find_element(By.ID, "new-password").send_keys("NewPass456!")
    driver.find_element(By.CSS_SELECTOR, "button[type='submit']").click()
    wait.until(EC.url_contains("/login"))

GitHub Actions: Full CI Pipeline

yaml

# .github/workflows/selenium-e2e.yml
name: Selenium E2E

on: [push, pull_request]

jobs:
  selenium:
    runs-on: ubuntu-latest
    timeout-minutes: 15

    steps:
      - uses: actions/checkout@v4

      - name: Set up Python
        uses: actions/setup-python@v5
        with:
          python-version: '3.12'

      - name: Install Chrome
        run: |
          wget -q -O - https://dl.google.com/linux/linux_signing_key.pub | sudo apt-key add -
          sudo apt-get update && sudo apt-get install -y google-chrome-stable

      - name: Install dependencies
        run: pip install selenium pytest pytest-asyncio pytest-xdist freecustom-email

      - name: Run Selenium tests (4 parallel)
        env:
          FCE_API_KEY: ${{ secrets.FCE_API_KEY }}
          APP_URL: ${{ vars.STAGING_URL }}
        run: pytest -n 4 tests/ -v --tb=short

OTP via CLI in Shell-Based Selenium Setups

If you are running Selenium from shell scripts rather than a test framework, the fce CLI is simpler than the API:

bash

#!/usr/bin/env bash
set -euo pipefail

# Create inbox
INBOX=$(fce inbox add random | tr -d '[:space:]')
echo "Test inbox: $INBOX"

# Trigger signup via your app (Selenium would do this in the browser)
# ... selenium or curl triggers signup ...

# Extract OTP
OTP=$(fce otp "$INBOX" | grep "OTP   ·" | awk '{print $NF}')
echo "OTP: $OTP"

# Continue Selenium test with the OTP
# ... selenium fills OTP field ...

# Cleanup
fce inbox remove "$INBOX"

More on CLI-based CI automation: How to Automate Email Verification Testing in CI/CD Pipelines (2026)


Handling Real-World Edge Cases

Custom domains to bypass disposable email blockers

Some apps reject known disposable email domains. On the Growth plan, you can register and verify your own domain:

python

# Use your own domain for test inboxes
inbox = f"selenium-{int(time.time())}@tests.yourdomain.com"
await fce.inboxes.register(inbox)

See Private or Custom Domains for setup instructions.

If your app sends a magic link rather than a code, the FCE API returns it in the verification_link field:

python

# Python SDK — get the full message
messages = await fce.messages.list(inbox)
if messages:
    msg = await fce.messages.get(inbox, messages[0].id)
    link = msg.verification_link  # full URL ready to navigate
    driver.get(link)

Slow staging environments

Add a retry loop or increase the SDK timeout:

python

otp = await fce.otp.wait_for(inbox, timeout=60)  # 60 second timeout

Comparison: FCE vs Common Alternatives for Selenium Testing

Approach

Fresh inbox per test

OTP extraction

Parallel safe

Real-time

CI-ready

FreeCustom.Email API

✓ Auto

✓ WebSocket

Mailinator API

✗ Manual

✗ Polling

Partial

Guerrilla Mail API

✗ Manual

Gmail + IMAP

Partial

✗ Manual

Mocked email

N/A

N/A

N/A

✓ but untrue


Frequently Asked Questions

Can I use fce watch WebSocket inside a Selenium test instead of polling? Yes, on Startup plan+. Use the SDK's client.realtime interface or the CLI's fce watch in a subprocess. For most Selenium tests, client.otp.waitFor() is simpler since it handles the wait internally.

Does the fresh inbox approach work with Selenium Grid? Yes. Each node creates its own inbox via the API. Inboxes are server-side resources — no local state is shared across nodes.

How fast is OTP delivery in practice? Under 200ms from the time the email hits the SMTP server. In practice with staging apps that have slight delays, you should set a 15–30 second timeout to account for the app's email sending latency, not FCE's delivery latency.

What if my app only allows work email addresses? Use a custom domain on Growth plan — your test inboxes will look like user@yourcompany.com. See Getting Around 'Work Email' Requirements Ethically (2026).

Is there a rate limit on inbox creation? Yes, per-plan. Growth allows 50 req/sec, 2M/month — more than enough for even very large Selenium suites. See freecustom.email/api/pricing for details.

Can I run this against a local dev server (localhost)? The FCE API works from anywhere. Your Selenium tests point at any URL — localhost, staging, or production. FCE receives emails independently of your test runner's network.

Do I need the CLI installed to use this with Selenium? No. The CLI is optional. The Python and JS SDKs talk directly to the REST API. The CLI is useful for shell-based scripts or quick local testing.


Written by

D

Dishant Singh

A full stack developer with good knowledge of email server, SEO, proxies, and networking, have more than 3 years of experience in building webapps for the netizens. Developing open source, fast, and free SaaS for all.

FAQ

Frequently Asked Questions

Does the fresh inbox approach work with Selenium Grid?+

Yes. Each node creates its own inbox via the API. Inboxes are server-side resources — no local state is shared across nodes.

How fast is OTP delivery in practice?+

Under 200ms from the time the email hits the SMTP server. In practice with staging apps that have slight delays, you should set a 15–30 second timeout to account for the app's email sending latency, not FCE's delivery latency.

What if my app only allows work email addresses?+

Use a custom domain on Growth plan — your test inboxes will look like user@yourcompany.com. See Getting Around 'Work Email' Requirements Ethically (2026).

Is there a rate limit on inbox creation?+

Yes, per-plan. Growth allows 50 req/sec, 2M/month — more than enough for even very large Selenium suites. See freecustom.email/api/pricing for details.

Can I run this against a local dev server (localhost)?+

The FCE API works from anywhere. Your Selenium tests point at any URL — localhost, staging, or production. FCE receives emails independently of your test runner's network.

Do I need the CLI installed to use this with Selenium?+

No. The CLI is optional. The Python and JS SDKs talk directly to the REST API. The CLI is useful for shell-based scripts or quick local testing.

Discussion0

No comments yet. Be the first to share your thoughts.