Test your app's emails without parsing a single email
Use a fresh address in every test. Wait for the email, then read the code or link it contains.
What it does
Three things you get, in plain words.
A new address for every test
Any address on your private mailbay.email domain reaches your catch-all inbox. Make one up in the test. There is nothing to set up first.
Waits for the email
One call waits until the next matching email arrives. You can match on the sender, the subject or the exact address it was sent to.
Hands you the code or link
The one-time codes and links in the email come back ready to use. Each message also shows its sender checks (SPF, DKIM and DMARC), a spam score and its attachments.
Wait for the email, then use what is in it
Your test fills in a new address, waits, and gets the code back. Choose a fresh inbox per test, or use the catch-all and invent addresses.
import { expect, test } from "@playwright/test";
const API = "https://api.mailbay.dev";
const headers = {
Authorization: `Bearer ${process.env.MAILBAY_API_KEY}`,
"Content-Type": "application/json",
};
test("sign up with an emailed code", async ({ page }) => {
// A fresh inbox just for this test
const inbox = await (
await fetch(`${API}/v1/inboxes`, {
method: "POST",
headers,
body: JSON.stringify({ local_part: `signup-${Date.now()}` }),
})
).json();
await page.goto("/signup");
await page.getByLabel("Email").fill(inbox.address);
await page.getByRole("button", { name: "Send code" }).click();
// Wait for the email. The code is already pulled out for you.
const res = await fetch(`${API}/v1/inboxes/${inbox.id}/wait?timeout=60`, { headers });
const { codes } = await res.json();
await page.getByLabel("Code").fill(codes[0]);
await page.getByRole("button", { name: "Continue" }).click();
await expect(page.getByRole("heading", { name: "Welcome" })).toBeVisible();
// Clean up
await fetch(`${API}/v1/inboxes/${inbox.id}`, { method: "DELETE", headers });
});# 1. Find your private domain and your catch-all inbox (once per test run)
curl https://api.mailbay.dev/v1/me -H "Authorization: Bearer $MAILBAY_API_KEY"
# "shared_domain": "k7m3qz.mailbay.email"
curl https://api.mailbay.dev/v1/inboxes -H "Authorization: Bearer $MAILBAY_API_KEY"
# the one with "catch_all": true, for example ibx_01jbz8w4k2q9
# 2. Sign up in your app with any new address, for example
# signup-42@k7m3qz.mailbay.email. No setup needed.
# 3. Wait for the email sent to exactly that address (up to 60 seconds)
curl "https://api.mailbay.dev/v1/inboxes/ibx_01jbz8w4k2q9/wait?to=signup-42@k7m3qz.mailbay.email&timeout=60" \
-H "Authorization: Bearer $MAILBAY_API_KEY"
# {"message": {...}, "codes": ["482913"], "links": ["https://app.example.com/verify?..."]}# Your app sends through Sukhovei Mail. Point a test at these addresses:
# delivered@test.mailbay.dev -> a "delivered" event
# bounced@test.mailbay.dev -> a "bounced" event (a permanent 550 5.1.1)
# complained@test.mailbay.dev -> a "complained" event (a spam report)
curl -X POST https://api.mailbay.dev/v1/emails \
-H "Authorization: Bearer $MAILBAY_API_KEY" \
-H "Content-Type: application/json" \
-d '{"from": "you@yourdomain.com", "to": "bounced@test.mailbay.dev", "subject": "Bounce test", "text": "Hello"}'It works the same from Cypress, Jest, Vitest or any tool that can make an HTTP request. If a wait finds nothing in time, it answers wait_timeout. Calling it again is safe.
Who it is for
Pick the page closest to what you are building.
Limits, honestly
What it does not do, so you can decide before you start.
- A wait lasts at most 120 seconds. If your app is slower, call it again.
- Sukhovei Mail tests email only. There is no SMS or phone number testing.
- There is no recorder or browser extension. You write the test in your own tool.
- The simulated outcomes (delivered, bounced, complained) test your sending side only. They do not send real mail and are not added to your suppression list.
- Mail is kept until you delete it, unless you set a retention period. Received mail does not count against your sending allowance.
- The free plan includes 3 named inboxes. Catch-all inboxes do not count toward that.
Questions
How do I test a sign-up email?
Sign up in your test with a new address on your mailbay.email domain. Then call the wait endpoint for that address. It returns the email with any one-time code or link already extracted, so you do not need to read the HTML.
Do I need to create an inbox for each test?
No. Any address on your private domain reaches your catch-all inbox, so a test can invent one. If you prefer, create a named inbox per test with POST /v1/inboxes and delete it afterwards.
Can I test bounces and spam complaints?
Yes, for emails you send through Sukhovei Mail. Send to delivered@, bounced@ or complained@ on test.mailbay.dev. Sukhovei Mail records that outcome without sending real mail, and does not add the address to your suppression list.
Does it work with Playwright and Cypress?
Yes. It is a plain HTTP API, so it works from Playwright, Cypress, Jest, Vitest or any script. There is a Playwright example on this page.
Can I test SMS or phone codes?
No. Sukhovei Mail tests email only.
Try it on the free plan.
The free plan has 3 inboxes, 3,000 emails a month and 3 custom domains. No card needed.