Skip to content

EMAIL TESTING · BETA

Wait for the email. Read the code.

Send to anything@your-private-domain; the address exists the moment you use it. Your signup test blocks on one API call until the message lands, then reads the one-time code or the link from the response.

Email testing · Example workspace

WHAT IT DOES

A domain you own. A code you don't have to scrape.

Everything an auth test needs from email, as API calls.

Any address, private domain.

Your workspace gets its own domain on devhelmmail.com. Every test run can use a fresh address on it, with no create-inbox call.

Codes and links, extracted.

Every message comes back with its one-time code and links already pulled out. Your test reads otp[0].value, not an HTML body.

Inspect and inject.

When a test fails, open the message: headers, body, attachments. Need a message without sending one? Inject it through the API.

THE WAIT API

Your signup test waits for the real email

Use a fresh address per run, trigger the email, then make one call. It returns as soon as the message lands, so there is no sleep to tune.

  • Fresh per run. The inbox is the part before the @, so signup-${Date.now()}@your-domain keeps parallel CI jobs apart.
  • One call. POST /api/v1/email/wait with the full address. Default timeout 30 s, maximum 120 s.
  • No trigger-then-wait race. An email that landed just before you called wait still counts.
  • A failure your test can read. No email within your timeout returns 408 WAIT_TIMEOUT, so the test fails with "verification email never arrived".
import { test, expect } from '@playwright/test';

const API = 'https://api.devhelm.io/api/v1';
const DOMAIN = process.env.DEVHELM_MAIL_DOMAIN!; // your domain on devhelmmail.com

test('signup with emailed one-time code', async ({ page, request }) => {
  test.setTimeout(60_000);
  const to = `signup-${Date.now()}@${DOMAIN}`;

  await page.goto('https://staging.acme.dev/signup');
  await page.getByLabel('Email').fill(to);
  await page.getByRole('button', { name: 'Create account' }).click();

  const res = await request.post(`${API}/email/wait`, {
    headers: {
      Authorization: `Bearer ${process.env.DEVHELM_API_TOKEN}`,
      'x-phelm-workspace-id': process.env.DEVHELM_WORKSPACE_ID!,
    },
    data: { to, timeoutMs: 30_000 },
    timeout: 35_000,
  });
  expect(res.status(), 'verification email never arrived').toBe(200);

  const { message } = await res.json();
  await page.getByLabel('Verification code').fill(message.otp[0].value);
  await page.getByRole('button', { name: 'Verify' }).click();
  await expect(page).toHaveURL(/\/welcome/);
});
200 · otp 418302408 · WAIT_TIMEOUT after 30 s

PRIVATE DOMAIN

Not a shared Gmail. Not a public inbox.

Most teams end up with a QA Gmail account or a disposable-mail domain anyone can read. A private domain avoids both.

  • The domain belongs to the workspace, not to whoever set up the QA mailbox.
  • Only your workspace can read mail sent to your domain. Guessing an address gets nobody in.
  • Your own domain on Team and above. Point the MX record for mail.acme-test.com at DevHelm and use addresses like reset@mail.acme-test.com.

Test run

signup.spec.ts

Unique address

signup+7f3a@…

Verified code

418302

A single workflow, with evidence at every step.

INSPECT

"Staging didn't send the email." Open it and see.

When a wait fails, see the mail that arrived, or see that nothing did.

  • Read the text, the HTML, every header and the attachments.
  • See the code and links DevHelm extracted, next to the message they came from.
  • Inject a test message with POST /api/v1/email/domains/{domain}/messages/inject to try your parser or UI without sending real email.
Email testing · Example workspace

SETUP

From API key to a green signup test

Once the DevHelm team turns on your workspace, three steps take the Playwright signup test you already have to green.

  1. 1

    Get your domain.

    Create an API key, copy your workspace ID, and add your private domain on devhelmmail.com.

  2. 2

    Point your test at it.

    Sign up with signup-${Date.now()}@your-domain in staging.

  3. 3

    Add the wait.

    Paste the snippet above. The first matched wait returns the email and the code.

$ export DEVHELM_API_TOKEN=dh_live_xxxxxxxx
$ export DEVHELM_WORKSPACE_ID=1234
$ export DEVHELM_MAIL_DOMAIN=acme-qa.devhelmmail.com
$ npx playwright test signup.spec.ts
  ✓ signup with emailed one-time code
  1 passed

FIT CHECK

Is this the right email testing tool?

It is built for tests that need to read real email in CI.

Strong fit when

  • Your signup, reset or invite tests currently sleep, scrape Gmail, or share one QA mailbox.
  • Your app sends through SES, Postmark, Resend or an auth provider, and you want to test the real message end to end.
  • Security will not allow a public disposable-mail domain.

Not the best fit when

  • You only need to catch mail your app sends to localhost. A local SMTP catcher does that job.
  • You need to send email, test SMS, or receive phone verification codes. Email Testing receives mail only.
  • You want a public inbox anyone can read. Every address here is private to your workspace.

PRICING

A free plan. Paid plans from $19 a month.

Testing is its own plan, on the same subscription as monitoring. Add it to any monitoring plan, Free included.

Free

$0

  • 1 email domain
  • 3 webhook inboxes
  • 300 stored events / mo
  • 3-day event history

Developer

$19 / mo

  • 1 email domain
  • 10 webhook inboxes
  • 3,000 stored events / mo
  • 7-day event history

Team

$79 / mo

  • 3 email domains
  • Your own domain (custom MX)
  • 100 webhook inboxes
  • 100,000 stored events / mo
  • 30-day event history

Business

$149 / mo

  • 10 email domains
  • Your own domain (custom MX)
  • 500 webhook inboxes
  • 300,000 stored events / mo
  • 90-day event history

Enterprise: unlimited domains, inboxes and events, 365-day history, custom pricing. Stored events count email and webhook together. Annual billing: two months free.

FAQ

Frequently asked questions

Addresses, codes, privacy and pricing.

How do I test OTP emails in Playwright?

Use a fresh address per run, such as signup-${Date.now()}@your-domain, trigger the email, and call the wait endpoint for that address. The response includes the extracted code; fill it into your form.

How do I test magic links?

Same flow: wait for the message, then page.goto the extracted link. No HTML parsing in your test.

Do I have to create an inbox before sending to it?

No. Every address at your private domain already exists. Use any string before the @.

What happens if the email never arrives?

The wait call returns HTTP 408 WAIT_TIMEOUT after your timeout (default 30 s, maximum 120 s). It never returns an empty success. Open the domain in the dashboard to see whether anything arrived for that address.

Is it private, or a public disposable inbox?

Private. Each workspace gets its own domain, and only that workspace can read its mail.

Can I use my own domain?

On Team and above, yes: add your domain, point its MX record at DevHelm, and verify it.

Is it free? What does it cost?

There is a free plan: 1 domain, 300 stored events a month and 3 days of history. Paid plans are $19, $79 and $149 per month, and annual billing gets two months free.

Stop reading the QA Gmail. Wait for the email.

The beta is open by invitation. Book a 30-minute demo and we'll set up your workspace.

$ curl -sS -X POST https://api.devhelm.io/api/v1/email/wait \
    -H "Authorization: Bearer $DEVHELM_API_TOKEN" -H "x-phelm-workspace-id: $DEVHELM_WORKSPACE_ID" \
    -H "Content-Type: application/json" \
    -d '{"to":"signup+7f3a@acme-qa.devhelmmail.com","timeoutMs":30000}' | jq -r '.message.otp[0].value'
418302
Wait for the email. Read the code. — DevHelm