MailSlurp logo

Email and SMS integration testing guide

Documentation navigation
Search documentation

Learn MailSlurp testing from SDK setup and core objects to inbox creation, message waits, OTP and link extraction, attachments, SMS, MFA, and CI.

View MarkdownAgent setup

Test the messages your application actually sends. MailSlurp gives your test a real email address or phone number, waits for the expected message, and exposes its contents so you can verify an account, follow a password reset link, check an invoice, or complete an SMS challenge.

Choose your SDK and test framework

Use your existing test runner. The MailSlurp SDK runs alongside your browser, mobile, or API test and handles inboxes and messages.

Language SDK setup Test framework guides
JavaScript and TypeScript JavaScript and TypeScript Playwright, Cypress, WebdriverIO, CodeceptJS, TestCafe
Java and Kotlin Java, Kotlin JUnit, Selenium, TestNG
Python Python Pytest, Robot Framework
C# and .NET C# Playwright, Selenium, NUnit SMS example
PHP PHP PHPUnit, Laravel example, Codeception example
Ruby Ruby RSpec, Capybara and Cucumber
Go, Swift, and Dart Go, Swift, Dart Go tests, Swift tests, Dart tests

For other languages, start with all SDKs. For HTTP-based tools, use Postman, Bruno, Insomnia, ReadyAPI, or the REST API reference.

Info: Start with a complete browser test. The Playwright guide walks through creating an inbox, signing up, waiting for a matching email, extracting its code, and signing in. The Cypress guide explains the plugin and Cypress command chain.

Understand the core objects

Object What it represents What your test uses
Inbox A mailbox that can receive email and send through supported API or SMTP workflows. Put emailAddress into your app. Pass id as inboxId to MailSlurp.
Email preview A summary returned by list and multi-message wait methods. Use its id, sender, subject, and timestamps to select a message. Fetch the full email for its body.
Email A received message, including recipients, subject, body, headers, and attachment IDs. Assert contents or pass id as emailId to extraction and preview methods.
Attachment A file associated with a message. Fetch metadata and download content using its attachment ID.
Phone number A provisioned number that receives SMS. Put phoneNumber into your app. Pass id as phoneNumberId to SMS methods.
SMS A received text message associated with a phone number. Assert the sender and body, then extract the verification code.
TOTP device A virtual authenticator paired with your test user's MFA secret. Request a fresh code from its device ID when your app asks for an authenticator code.

An inbox ID is not an email address. A phone number ID is not the dialable phone number. These distinctions matter when passing values between an API response, a browser form, and a wait call.

Info: Explore the objects. The inbox guide covers creation, expiry, names, and types. The email guide covers full messages and sending. See attachments, phone numbers and SMS, and TOTP devices for their lifecycles.

Follow the testing lifecycle

Most email and SMS tests follow this sequence. The application action and final assertion change with the scenario; resource creation, waiting, and extraction stay the same.

Create or reserve a test inbox / phone number
    -> Give its address / number to your application
    -> Trigger signup, reset, purchase, or notification
    -> Wait for a message scoped to this test
    -> Assert sender, recipient, subject, and content
    -> Extract the code, link, or attachment
    -> Complete the action in your application
    -> Assert the final application state
    -> Clean up resources owned by this test

A successful send response proves that the sending request succeeded. Waiting proves receipt. Using the delivered code or link and checking your application proves the user flow works.

Set up the client

The code below uses the TypeScript SDK in a Node.js test process. Install it in your test project:

npm install --save-dev mailslurp-client

Set MAILSLURP_API_KEY in your shell or CI secret settings using a key from the dashboard. Keep SDK calls in the test process; do not put the key in your application's frontend bundle.

import assert from 'node:assert/strict';
import { MailSlurp } from 'mailslurp-client';

const apiKey = process.env.MAILSLURP_API_KEY;
if (!apiKey) throw new Error('Set MAILSLURP_API_KEY');
const mailslurp = new MailSlurp({ apiKey });

The following recipes reuse this client. When a block uses inbox, email, or since, it continues the preceding steps. Blocks labelled pseudocode describe actions you implement in your own application.

Create an isolated inbox

const inbox = await mailslurp.createInboxWithOptions({
  name: 'signup-test',
  expiresIn: 600_000,
});

assert.ok(inbox.id);
assert.ok(inbox.emailAddress);

Use inbox.emailAddress in the signup or notification form. A fresh inbox per test attempt is the simplest way to separate parallel tests. expiresIn is in milliseconds; choose an expiry longer than the entire test, retries, and any time needed to collect failure evidence.

For a fixed address, load the inbox by its saved ID or look it up by name or email address. Custom domains support addresses on a domain you control. Plus addressing lets multiple test recipients share a mailbox; match the full plus address when receiving those messages.

Trigger the application and wait for email

Capture the start time immediately before the action that sends the message:

const since = new Date();
// Submit your application's signup form with inbox.emailAddress here.

const email = await mailslurp.waitController.waitForLatestEmail({
  inboxId: inbox.id,
  since,
  timeout: 60_000,
  unreadOnly: true,
});

assert.ok(email.to.includes(inbox.emailAddress));
assert.ok(email.subject?.includes('confirm'));
assert.ok(email.body);

This waits up to 60 seconds and returns early when a qualifying email is available. It also considers messages that arrived between submitting the form and making the wait request. Set your overall test timeout higher than the wait timeout so setup, browser actions, and cleanup have time to finish.

waitForLatestEmail is useful when the test expects a single message. If signup sends both a welcome email and a verification email, match the verification message explicitly.

Match the right message

import { MatchOptionFieldEnum, MatchOptionShouldEnum } from 'mailslurp-client';

const verificationEmail = await mailslurp.waitController.waitForMatchingFirstEmail({
  inboxId: inbox.id,
  since,
  timeout: 60_000,
  unreadOnly: true,
  matchOptions: {
    matches: [{
      field: MatchOptionFieldEnum.SUBJECT,
      should: MatchOptionShouldEnum.CONTAIN,
      value: 'Please confirm your email address',
    }],
  },
});

Use this matching call instead of the latest-email call when selecting the verification email. Reading a full email marks it read; a second unread-only wait will not retrieve that same message.

You can combine subject, sender, and recipient filters. Use a unique order reference, test address, or run identifier where your application supports it. The wait's field filters select the email; extracting a value from the body is a separate step.

Match messages by meaning when templates vary

Subject and sender filters work well for stable templates. Add an AI prompt when the same verification message can arrive with different wording, languages, or layouts. Keep the inbox or phone scope, time boundary, and any known sender filters; describe the message's purpose in the prompt.

For example, select "the account signup verification email" and then extract its code. Keep checks such as "the code expires in ten minutes" as assertions on that selected message. A failing assertion should expose the broken email rather than select a different one.

The AI matching section in the wait-for guide shows both prompt-enabled existing waits and a combined wait, assertion, and extraction request. The AI guide covers response handling, reusable schemas, model selection, evidence, and usage limits.

Info: Choose a wait method. The wait-for guide explains latest versus matching messages, multiple messages, zero-based indexes, plus addresses, SMS filters, unread state, and timeout troubleshooting.

Extract an email OTP with a local regular expression

Match the surrounding label as well as the code format. This avoids taking an unrelated number from a date, phone number, or footer.

const match = /verification code is\s+(\d{6})\b/i.exec(email.body ?? '');
assert.ok(match, 'Expected a six-digit verification code in the email');
const code = match[1];
assert.match(code, /^\d{6}$/);

Keep the code as a string so a leading zero survives. Adapt the label and length to your application's actual message. Then enter code in the verification form and assert that the user is verified or signed in.

Extract a capture group through the API

HTTP clients and low-code tools can ask MailSlurp to extract the code without parsing the body themselves:

const result = await mailslurp.emailController.getEmailContentMatch({
  emailId: email.id,
  contentMatchOptions: {
    pattern: 'verification code is\\s+(\\d{6})\\b',
  },
});
assert.ok(result.matches.length > 1, 'Expected a regex capture group');
const extractedCode = result.matches[1];
assert.match(extractedCode, /^\d{6}$/);

matches[0] is the full match; matches[1] is the first capture group. The API uses Java regular-expression syntax. Escape backslashes when putting a regex in a JSON or TypeScript string.

Info: Follow a matching-and-extraction example. The HTTP OTP example and Java OTP example show the full create, signup, match, extract, confirm, and login sequence.

Use the dedicated code extraction endpoints

MailSlurp also provides getEmailCodes and getSmsCodes. These return a best code candidate, ranked candidates, the method used, and warnings. Set MAILSLURP_EMAIL_ID to the ID returned by your email wait:

curl --fail-with-body --silent --show-error \
  --request POST "https://api.mailslurp.com/emails/$MAILSLURP_EMAIL_ID/codes" \
  --header "x-api-key: $MAILSLURP_API_KEY" \
  --header 'Content-Type: application/json' \
  --data '{"method":"PATTERN","allowFallback":false,"minLength":6,"maxLength":6,"maxCandidates":3}'

For SMS, use POST /sms/{smsId}/codes after waiting for the message. Check found, validate code against your expected format, and inspect candidates, methodUsed, and warnings before accepting a result. Use customPatterns when your template has a known label. Extraction options also expose AI and OCR strategies; inspect the actual method used and disable fallback when a test requires a specific strategy.

Use the link extraction endpoint to parse HTML anchors. Select the intended URL by host and path instead of blindly opening the first link, which could be a logo or unsubscribe link.

const { links } = await mailslurp.emailController.getEmailLinks({ emailId: email.id });
const expectedOrigin = 'https://app.example.com'; // Your test application's origin.
const actionLinks = links.filter((link) => {
  const url = new URL(link);
  return url.origin === expectedOrigin && url.pathname === '/reset-password';
});
assert.equal(actionLinks.length, 1, 'Expected one password reset link');
const resetLink = actionLinks[0];
// Open resetLink with your browser test and complete the reset form.

For a magic link, continue in the browser context that requested the link when the application binds it to cookies or a login session. For mobile email links, open the link on the test device, return to the app, and check the updated user state.

Info: Test passwordless and mobile sign-in. See the NextAuth magic-link guide, Firebase example, and Android Appium/Espresso examples.

Query HTML content

Use a CSS selector when your email template has a stable element for a value such as an order number or total:

const query = await mailslurp.emailController.getEmailHTMLQuery({
  emailId: email.id,
  htmlSelector: '[data-order-id]',
});
assert.ok(query.lines.includes('ORDER-1042'));

This returns matching text lines. Use link extraction for anchor URLs and attachment methods for files. Prefer assertions on meaningful text and values to a whole-body equality check when timestamps, tracking URLs, or MIME formatting legitimately change.

Wait and extract with AI

Use extractionPreset: 'OTP_CODE' to receive data.code as a string without defining a schema. It preserves leading zeros, letter case, and separators. For other values, such as an invoice total or reset URL, provide an outputSchema instead.

This alternative continues from the inbox and since captured before sending. It uses Playwright's request fixture inside a test; expect comes from @playwright/test. Set MAILSLURP_BASE_PATH when running against another API environment.

import { randomUUID } from 'node:crypto';

const baseUrl = (process.env.MAILSLURP_BASE_PATH ?? 'https://api.mailslurp.com').replace(/\/$/, '');
const response = await request.post(`${baseUrl}/ai/messages/wait`, {
  headers: { 'x-api-key': apiKey, 'Idempotency-Key': randomUUID() },
  timeout: 130_000,
  data: {
    scope: { inboxIds: [inbox.id] },
    since: since.toISOString(),
    timeout: 120_000,
    match: { prompt: 'The account signup verification email' },
    extractionPreset: 'OTP_CODE',
  },
});
await expect(response).toBeOK();
const result = await response.json();
expect(result.successful, result.summary).toBe(true);
expect(result.data?.code).toMatch(/^\d{6}$/);
const code = result.data.code;
// Enter code in your app and assert that signup succeeds.

The six-digit assertion belongs to this application's format; the preset also supports alphanumeric codes. Missing or equally eligible codes return INCONCLUSIVE with null data and an explanation in summary. A successful HTTP response alone does not mean the evaluation passed. For SMS, change the scope to phoneNumberIds: [phone.id].

Info: Run a complete AI test. The AI guide's Playwright example covers setup, signup, extraction, verification, sign-in, and cleanup. See AI waits for message selection and retries, and output schemas for custom fields.

Check attachments and message content

For receipts, CSV exports, invoices, or reports, verify both the attachment metadata and the downloaded content. An attachment count alone will not catch an empty file or the wrong customer's invoice.

assert.equal(email.attachments?.length, 1, 'Expected one attachment');
const attachmentId = email.attachments![0];
const metadata = await mailslurp.attachmentController.getAttachmentInfo({ attachmentId });
assert.equal(metadata.name, 'report.csv');
assert.equal(metadata.contentType, 'text/csv');

const download = await mailslurp.attachmentController.downloadAttachmentAsBase64Encoded({
  attachmentId,
});
const csv = Buffer.from(download.base64FileContents, 'base64').toString('utf8');
assert.ok(csv.includes('ORDER-1042'));

For binary files, compare a known checksum or parse the format with a PDF, spreadsheet, or image library in your test process. If your test sends the file, upload its Base64 contents, filename, and MIME type first, then use the returned attachment IDs in sendEmail.

For the message itself, check the expected sender, recipient, subject, personalization, and application reference. Headers and raw MIME help diagnose reply addresses, encoding, or content-type differences.

Info: Verify files end to end. The attachment guide covers uploads and downloads. The named-attachment example checks filename preservation, and the Python PyUnit example demonstrates matching attachments and downloading them.

Test SMS and authenticator MFA

Receive an SMS verification code

Provision a phone number in MailSlurp and save its ID as MAILSLURP_PHONE_NUMBER_ID. Reserve that number for the test or worker using it; do not choose an arbitrary first number from a shared account.

const phoneNumberId = process.env.MAILSLURP_PHONE_NUMBER_ID;
if (!phoneNumberId) throw new Error('Set MAILSLURP_PHONE_NUMBER_ID');
const phone = await mailslurp.phoneController.getPhoneNumber({ phoneNumberId });

const smsSince = new Date();
// Submit phone.phoneNumber to your application's SMS verification flow here.
const sms = await mailslurp.waitController.waitForLatestSms({
  waitForSingleSmsOptions: {
    phoneNumberId: phone.id,
    since: smsSince,
    timeout: 60_000,
    unreadOnly: true,
  },
});
const smsMatch = /Your code is:\s*(\d{6})\b/.exec(sms.body);
assert.ok(smsMatch, 'Expected an SMS verification code');
const smsCode = smsMatch[1];
// Submit smsCode, then assert that the application accepted the challenge.

Adapt the code pattern to the sender's message and the phone format to your application's country-code fields. For multiple SMS types on one number, use waitForSms with sender/body filters and a time window. A pre-provisioned phone is a persistent resource: release its test reservation afterward instead of deleting a number another suite needs.

Info: Build an SMS test. See phone and SMS setup, the Playwright SMS example, the Cypress SMS example, and C# NUnit SMS OTP.

Generate a TOTP authenticator code

An authenticator challenge uses a shared secret and time, rather than an incoming email or SMS. Pair a MailSlurp TOTP device with the secret or otpauth:// URL presented during your test user's MFA enrollment. Request a fresh device code when the application displays the challenge.

Pseudocode:

Create a test user and begin MFA enrollment
Read the enrollment secret, otpauth URL, or QR code
Create the corresponding MailSlurp TOTP device
Request a device code and submit it to complete enrollment
Sign out, then sign in as the same user
Request a fresh code from that same device
Submit it and assert authenticated access

Info: Complete MFA enrollment and login. The TOTP guide explains pairing, code timing, and the difference between enrollment and later sign-in. The Auth0 example demonstrates Selenium and Playwright flows.

Send test email and exercise SMTP or IMAP

To test an inbound email handler, send a controlled message from a MailSlurp inbox to your application's test address, then assert the resulting ticket, reply, webhook, or database record.

To check your MailSlurp setup alone, send a message to the inbox you created:

await mailslurp.sendEmail(inbox.id, {
  to: [inbox.emailAddress],
  subject: 'MailSlurp setup check',
  body: 'This message verifies the send and receive connection.',
});
const received = await mailslurp.waitController.waitForMatchingFirstEmail({
  inboxId: inbox.id,
  timeout: 60_000,
  unreadOnly: true,
  matchOptions: {
    matches: [{
      field: MatchOptionFieldEnum.SUBJECT,
      should: MatchOptionShouldEnum.EQUAL,
      value: 'MailSlurp setup check',
    }],
  },
});
assert.equal(received.from, inbox.emailAddress);

For an application delivery test, trigger your application's sender using the created recipient. Sending the test message directly through MailSlurp checks a different path.

If the application sends through SMTP, create an SMTP_INBOX, obtain its IMAP/SMTP access details, and configure the application's mail transport with the returned host, port, username, and password. Use the TLS settings appropriate to that endpoint. Receive and assert the result through the API, or connect by IMAP when mailbox listing, flags, search, or message fetching are the behavior under test.

Info: Test your existing mail transport. The SMTP/IMAP guide explains access configuration. Examples include Nodemailer, .NET MailKit, Java Jakarta Mail, and React Email templates.

Choose assertions for your scenario

Scenario Message checks Application checks
Signup verification Correct recipient, verification subject, one usable code or link. User becomes verified and can sign in.
Password reset New reset message for this attempt, expected link host and path. New password works; old password and reused reset token behave as specified.
Magic link Correct account and action link. The requesting browser reaches authenticated content.
Resend or expired OTP Match the new attempt; retain the earlier message ID separately. Old, incorrect, expired, and reused codes are rejected according to your policy.
Newsletter signup Welcome message, subscriber name, expected links and content. Subscription state is correct; a double opt-in link confirms the subscription when used.
Purchase or notification Order reference, recipient, currency, totals, attachment contents. Order or event state agrees with the message.
Reply or inbound routing Sender, reply address, body, and attachments. Your handler creates the intended ticket, reply, or routed record.
Mobile authentication Email link or SMS for the test user. The device returns to the app with the expected authenticated state.

Negative tests need a defined observation window. A timeout can support an assertion that no matching message arrived during that window only after distinguishing it from authentication, network, and service errors. Likewise, receiving one message does not prove that no duplicate arrives later.

For event-driven handlers, register the webhook before triggering the message, correlate the event's message ID, fetch the message if needed, and assert your handler's result. Make repeated webhook deliveries safe to process without duplicating application actions.

Preview emails and check delivery at scale

Choose the check that answers your question:

Question MailSlurp feature What to inspect
Is the delivered HTML and content correct? Email preview and screenshots Open the stored email's preview URL or capture a screenshot for a controlled visual comparison.
Does the email render correctly in real clients? Device previews Wait for a render run, inspect each requested target, and review screenshots and failed targets.
Are links, images, and HTML features sound? Email audit and feature support Broken resources, supported features, and actionable content findings.
Did all test inboxes or phones receive the expected traffic? Deliverability tests Matching counts, sender/subject expectations, unmatched recipients, and final run status.
Does a real campaign reach inbox or spam folders? Inbox placement Send through your actual provider to the supplied seed addresses and inspect per-provider placement.
Has sending-domain configuration changed? Domain Monitor SPF, DKIM, DMARC, and other domain checks relevant to delivery.

An HTML preview is useful for inspecting content. Real-client renders answer client compatibility questions, while inbox placement measures mailbox folder outcomes. Keep these assertions distinct in your test reports.

For load tests, allocate recipients, start measurement, trigger traffic from your application, and collect outcomes within a defined deadline. Measure application request latency separately from message arrival time. Bound concurrency and message volume to your account's quotas and your test environment's capacity.

Info: Run delivery and rendering examples. Start with JMeter SMS deliverability, k6 email testing, Locust, or Gatling. The device-rendering example covers run creation, waiting, screenshots, and sharing; the inbox-placement example covers seed addresses and results. In load tests, choose the external-sender mode when measuring your application; simulator traffic exercises the test setup.

Keep tests reliable in CI

  • Isolate each attempt. Give each parallel test its own inbox. Reserve phone numbers per worker or serialize tests using a shared number.
  • Correlate each message. Combine resource ID, a start time captured before sending, and explicit subject/sender/recipient filters. Keep clocks synchronized if you use timestamp filtering.
  • Use bounded waits. Make HTTP client timeouts longer than the API wait, and runner timeouts longer than the whole scenario. Retry only transient failures within a fixed overall deadline.
  • Retain the message you fetched. Full-message reads change unread state. Reuse the returned object or fetch its ID explicitly instead of waiting for it again.
  • Make assertions specific. Check the delivered value and the final application result. Do not pass a test just because the email body is nonempty.
  • Collect useful failure evidence. Save resource and message IDs, timestamps, expected filters, and the failed assertion. Restrict access to reports containing message bodies, OTPs, or authentication links.
  • Clean up what the test owns. Use teardown or finally to delete temporary inboxes after collecting evidence. Expiry is a fallback if the runner stops unexpectedly. Report cleanup failures without hiding the original test failure.

Delete the inbox created for the scenario when it is no longer needed:

await mailslurp.inboxController.deleteInbox({ inboxId: inbox.id });

Watch testing walkthroughs

Follow an email, SMS, or MFA test from setup to assertion. Each video includes a written guide for current SDK setup and more examples.

Email confirmation with Playwright16:00

Create an inbox, capture the signup code, and finish account verification.

Playwright guide
Browse all testing videos
  1. Email confirmation with Playwright (16:00)
  2. Email verification codes in Cypress (9:13)
  3. Authenticator MFA with Selenium and Java (8:19)
  4. Password-reset emails with Node.js and Vitest (4:58)
  5. Read SMS messages in JavaScript (3:46)
  6. Email integration tests in Postman (15:21)

Find the next guide or example

Info: Browser and mobile tests. Use Playwright, Cypress, Selenium, WebdriverIO, CodeceptJS, or TestCafe for browser steps. Use the Android example for Appium and Espresso and the Swift SDK for native test code.

Info: API, BDD, and low-code workflows. Use JUnit, Pytest, PHPUnit, or Robot Framework with your existing assertions. For visual workflows, see ACCELQ, UiPath, MuleSoft, Testim, Reflect, Power Automate, and the n8n example. BrowserStack covers running browser tests remotely.

Browse the example project directory or source repository for additional languages and runners. Each project includes its own dependencies and setup; choose the SDK guide for installation in a new project, and use the example to follow the testing sequence.