DEV Community

Dishant Singh
Dishant Singh

Posted on

Stop Skipping Email Verification in Your Automated Tests

Every team I have talked to handles email verification the same way in their test suite: they skip it, mock it, or use a shared inbox that makes parallel tests unreliable. I did all three before I found an approach that actually works at scale.

The problem is that most disposable email services have no real developer tooling. You get a web UI and maybe a REST API that requires you to poll, parse, and regex your way to an OTP. Nothing is designed for automation.

FreeCustom.Email is different — it is the only disposable email service with an official CLI, official SDKs, a dedicated OTP extraction endpoint, and WebSocket real-time delivery. This post covers every way to use it for signup automation, from a three-line bash script to a full parallel Playwright test suite.


The Pattern

All signup automation comes down to three steps:

1. Create a fresh inbox        → get an address nobody else is using
2. Submit it in the signup UI  → your app sends the verification email
3. Extract the OTP             → complete verification
Enter fullscreen mode Exit fullscreen mode

The only question is which tool you use for each step. I will cover all of them.


Option 1: The CLI (Fastest for Local Dev and Scripts)

Install:

curl -fsSL freecustom.email/install.sh | sh   # macOS / Linux
npm install -g fcemail@latest                  # All platforms
choco install fce                              # Windows
brew tap DishIs/homebrew-tap && brew install fce  # Homebrew
Enter fullscreen mode Exit fullscreen mode

Full install options and all releases at github.com/DishIs/fce-cli. Login once:

fce login
# Browser → sign in → key saved to OS keychain automatically
Enter fullscreen mode Exit fullscreen mode

Automate a signup in four lines:

INBOX=$(fce inbox add random)
curl -s -X POST https://myapp.com/signup -d "email=$INBOX&password=Test1234!"
OTP=$(fce otp "$INBOX" | grep "OTP   ·" | awk '{print $NF}')
curl -s -X POST https://myapp.com/verify -d "email=$INBOX&otp=$OTP"
Enter fullscreen mode Exit fullscreen mode

The fce otp output:

────────────────────────────────────────────────
  OTP
────────────────────────────────────────────────

  OTP   ·  212342
  From  ·  noreply@myapp.com
  Subj  ·  Your verification code
  Time  ·  20:19:54
Enter fullscreen mode Exit fullscreen mode

For local development, fce dev is even faster — it creates an inbox and starts watching it in one command:

fce dev
# → dev-fy8x@ditcloud.info created and watching
# → emails appear in real time as they arrive
Enter fullscreen mode Exit fullscreen mode

Option 2: The TypeScript SDK

npm install freecustom-email
Enter fullscreen mode Exit fullscreen mode
import { FreecustomEmailClient } from 'freecustom-email';

const fce = new FreecustomEmailClient({ apiKey: process.env.FCE_API_KEY! });

async function automateSignup() {
  const email = `auto-${Date.now()}@ditmail.info`;
  await fce.inboxes.register(email);

  // Trigger signup
  await fetch(`${process.env.APP_URL}/api/signup`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ email, password: 'AutoTest123!' }),
  });

  // No polling loop. No regex. Just this:
  const otp = await fce.otp.waitFor(email, { timeout: 30_000 });
  console.log(`Got OTP: ${otp}`);

  // Complete verification
  await fetch(`${process.env.APP_URL}/api/verify`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ email, otp }),
  });

  await fce.inboxes.unregister(email);
}
Enter fullscreen mode Exit fullscreen mode

Running 10 signups in parallel is trivial because each has its own inbox:

await Promise.all(Array.from({ length: 10 }, () => automateSignup()));
Enter fullscreen mode Exit fullscreen mode

Option 3: The Python SDK

pip install freecustom-email httpx
Enter fullscreen mode Exit fullscreen mode
import asyncio, os, httpx
from freecustom_email import FreeCustomEmail

fce = FreeCustomEmail(api_key=os.environ["FCE_API_KEY"])
APP = os.environ["APP_URL"]

async def automate_signup():
    email = f"auto-{int(asyncio.get_event_loop().time())}@ditmail.info"
    await fce.inboxes.register(email)

    async with httpx.AsyncClient() as client:
        await client.post(f"{APP}/api/signup",
                          json={"email": email, "password": "AutoTest123!"})

        otp = await fce.otp.wait_for(email, timeout=30)
        print(f"OTP: {otp}")

        await client.post(f"{APP}/api/verify",
                          json={"email": email, "otp": otp})

    await fce.inboxes.unregister(email)

# Five parallel signups
asyncio.run(asyncio.gather(*[automate_signup() for _ in range(5)]))
Enter fullscreen mode Exit fullscreen mode

Option 4: Playwright (Browser-Based Flows)

When you need to interact with the actual UI rather than call APIs directly:

// tests/fixtures/fce.ts
import { test as base } from '@playwright/test';
import { FreecustomEmailClient } from 'freecustom-email';

const fce = new FreecustomEmailClient({ apiKey: process.env.FCE_API_KEY! });

export const test = base.extend({
  inbox: async ({}, use) => {
    const addr = `pw-${Date.now()}@ditmail.info`;
    await fce.inboxes.register(addr);
    await use(addr);
    await fce.inboxes.unregister(addr).catch(() => {});
  },
  getOtp: async ({ inbox }: any, use: any) => {
    await use((ms = 30_000) => fce.otp.waitFor(inbox, { timeout: ms }));
  },
});
Enter fullscreen mode Exit fullscreen mode
// tests/signup.spec.ts
import { test, expect } from './fixtures/fce';

test('signup flow', async ({ page, inbox, getOtp }: any) => {
  await page.goto('/signup');
  await page.fill('#email', inbox);
  await page.fill('#password', 'Test123!');
  await page.click('button[type="submit"]');
  await page.waitForSelector('#otp-input');

  const otp = await getOtp();
  await page.fill('#otp-input', otp);
  await page.click('button[type="submit"]');
  await expect(page).toHaveURL(/dashboard/);
});
Enter fullscreen mode Exit fullscreen mode

Option 5: Selenium (Python)

from selenium import webdriver
from selenium.webdriver.common.by import By
from freecustom_email import FreeCustomEmail
import asyncio, os, time

fce = FreeCustomEmail(api_key=os.environ["FCE_API_KEY"])
opts = webdriver.ChromeOptions()
opts.add_argument("--headless")
driver = webdriver.Chrome(options=opts)

inbox = f"sel-{int(time.time())}@ditmail.info"
asyncio.run(fce.inboxes.register(inbox))

driver.get("https://myapp.com/signup")
driver.find_element(By.ID, "email").send_keys(inbox)
driver.find_element(By.ID, "password").send_keys("Test123!")
driver.find_element(By.CSS_SELECTOR, "button[type='submit']").click()

otp = asyncio.run(fce.otp.wait_for(inbox, timeout=30))
driver.find_element(By.ID, "otp-input").send_keys(otp)
driver.find_element(By.CSS_SELECTOR, "button[type='submit']").click()

driver.quit()
asyncio.run(fce.inboxes.unregister(inbox))
Enter fullscreen mode Exit fullscreen mode

Option 6: OpenClaw AI Agent (Zero Code)

This one surprised me with how well it works. With any AI agent that can run shell commands, just describe the task:

"Automate a signup at https://myapp.com — create a temp inbox, fill the form, extract the OTP, and complete verification. The fce CLI is already logged in."

The agent figures out the right commands and runs them. Great for one-off tasks or when you want automation without writing a script.


GitHub Actions — Parallel Jobs with Matrix Strategy

jobs:
  signup-automation:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        shard: [1, 2, 3, 4]

    steps:
      - uses: actions/checkout@v4

      - name: Install fce CLI
        run: curl -fsSL freecustom.email/install.sh | sh

      - name: Run signup automation (shard ${{ matrix.shard }})
        env:
          FCE_API_KEY: ${{ secrets.FCE_API_KEY }}
          APP_URL: ${{ vars.STAGING_URL }}
        run: node scripts/signup-automation.js

      - name: Cleanup on failure
        if: failure()
        env:
          FCE_API_KEY: ${{ secrets.FCE_API_KEY }}
        run: fce inbox list | grep "auto-" | xargs -I{} fce inbox remove {} || true
Enter fullscreen mode Exit fullscreen mode

Handling Apps That Reject Disposable Domains

Some signup forms check the email domain and reject known temp mail addresses. Two options:

Custom domain (Growth plan): Register your own domain in the FCE dashboard. Then test-001@automation.yourcompany.com is a valid FCE inbox. No blocklist problems.

Use less-known FCE domains: Run fce domains to see all domains available on your plan. Some are newer and not on common blocklists.


Approach Comparison

Approach OTP extraction Parallel safe CI-ready Skill needed
fce CLI + bash Auto Bash
JS/TS SDK Auto TypeScript
Python SDK Auto Python
Playwright fixture Auto Playwright
Selenium fixture Auto Selenium
OpenClaw AI agent Auto Partial None
Shared inbox (old way) Manual regex Regex suffering

Links


What is your current setup for testing email verification flows? I am especially curious whether anyone has built something with the AI agent approach — it still feels a bit like magic to me every time it works.

Top comments (0)