Groundmail

Email testing

Catch every test email. Read the code in one line.

A hosted test inbox for your end-to-end tests. Your app sends signup codes, password resets and magic links with whatever provider it uses today; your test waits for the email and reads the code and links. Nothing is delivered.

Any test runner

Sign up, wait, read the code

A fresh address per test, one call to wait for the email, and the verification code already pulled out of the HTML. Playwright, Vitest, Jest, Cypress, Selenium, pytest or a plain HTTP call from any language: it's the same three steps.

import { test, expect } from "@playwright/test";
import { Groundmail } from "groundmail";

const gm = new Groundmail(process.env.GROUNDMAIL_API_KEY!);

test("sign up with an email code", async ({ page }) => {
  const to = await gm.testInbox.address({ prefix: "signup" });
  // → signup-k3m9x2pq@acme.test.groundmail.dev

  await page.goto("https://staging.example.com/signup");
  await page.fill("#email", to);
  await page.click("text=Send code");

  const email = await gm.testInbox.waitFor({ to });
  await page.fill("#code", email.code!);
  await page.click("text=Verify");

  await expect(page).toHaveURL(/dashboard/);
});

How it works

Three steps, any provider

Your app doesn't change. Point a test user's email at your test domain and wait for it.

1. Get your test domain

Every team gets one: <team>.test.groundmail.dev. Any address at it is yours, so every test can use its own.

2. Send from your app as usual

With SendGrid, Resend, SES, plain SMTP or Groundmail, from staging, a preview deploy or CI. The mail is caught for your team and never delivered.

3. Wait for it in your test

waitFor returns the email with the code and links extracted. Or open the Test inbox in the dashboard to see the HTML, text, source and headers.

Built for CI

Works wherever your tests run

The test inbox lives on the internet, not on your laptop, so a deployed app can reach it.

  • Works from staging, preview deploys and CI runners
  • Each test gets its own address, so parallel runs never collide
  • Long-poll API: no sleep-and-retry loops in your tests
  • Code (4 to 8 digits) and links extracted from every email
  • HTML, text, source and headers in the dashboard
  • Emails kept 14 days

Any language: long-poll the API

curl "https://api.groundmail.dev/test-inbox/wait?to=signup-k3m9x2pq@acme.test.groundmail.dev&timeout=30" \
  -H "Authorization: Bearer $GROUNDMAIL_API_KEY"

{
  "email": {
    "id": "tie_8f3a…",
    "subject": "Your Acme code",
    "code": "482913",
    "links": ["https://staging.example.com/verify?token=…"],
    …
  }
}

From a shell or CI step

groundmail test-inbox wait --to signup-k3m9x2pq@acme.test.groundmail.dev

FAQ

Common questions

Is anything delivered to a real inbox?

No. Mail to your test domain is caught and stored for your team, and never delivered or forwarded anywhere. Unknown test domains are refused.

Which email providers does it work with?

Any of them. The test inbox is just a domain that receives mail, so it works when your app sends with SendGrid, Resend, SES, Postmark, Mailgun, your own SMTP server, or Groundmail. You don't need to switch providers to use it.

How do parallel CI runs avoid grabbing each other's email?

Give every test its own address. Anything at your test domain lands in your inbox, so gm.testInbox.address() hands out a fresh one like signup-k3m9x2pq@acme.test.groundmail.dev, and waitFor only returns mail sent to that address.

What do I get back for each email?

The HTML and text bodies, the full source and headers, plus the one-time code (4 to 8 digits) and every link, extracted for you. Read email.code or email.links and skip the HTML parsing.

How long are test emails kept?

14 days. After that they are deleted.

How much does it cost?

Every account gets 100 test emails a month free, on any plan. At the limit, new test emails are refused until the next month. The email testing add-on is $10/mo with 5,000 included, then $2 per 1,000. Test emails never count against your sending volume.

I already send with Groundmail. Do test keys work too?

Yes. Emails sent with a gm_test_ API key are never delivered, and they show up in the same test inbox, so the same waitFor call works for them.

Can I use it without the Node SDK?

Yes. GET /test-inbox/wait?to=… long-polls until the email arrives (30 seconds by default, up to 60) and returns it as JSON, so any language or test runner can use it. From a shell or a CI step, run groundmail test-inbox wait --to <address>.

Test the emails your app sends.

100 test emails a month free on every account. When you're ready to send for real, it's the same account.