Article featured image
How to test order confirmation emails in Cypress
8min readLast updated: April 28, 2026

Order confirmation emails are one of those things that feel simple until they break in production.

Your checkout flow works. The payment goes through. The success page loads. But somewhere between your app and the customer's inbox, something goes wrong. The email never arrives, the order total is off, or the "View Order" link points to a 404. And because most test suites stop at the UI, nobody catches it until a real customer does.

The problem is that email is async, dynamic, and awkward to test. You can't assert an inbox the same way you assert a DOM element. CI environments don't play nicely with real email providers. Gmail and Outlook bring OAuth headaches, rate limits, and automated abuse detection that make them unreliable for automated runs. And when email delivery fails silently, your app doesn't always know.

This guide walks through how to test order confirmation emails end-to-end using Cypress and testmail.app. You'll be able to assert that the right email was sent, the dynamic content is correct, and the links actually work, all within your existing CI pipeline.

Types of confirmation emails

Not all confirmation emails are the same. Before writing tests, it helps to know which type you're dealing with — each one has different dynamic data and failure modes worth asserting against.

Transaction confirmations (order confirmation, payment receipt, refund) contain the highest-stakes data: totals, taxes, payment method, and transaction IDs. These are the ones customers screenshot and forward to support when something goes wrong. A missing or incorrect email here directly impacts trust.

Example of an order confirmation email

Subscription confirmations (new plan, upgrade, cancellation) focus on plan name, billing cycle, and renewal date. A wrong amount or date doesn't just confuse customers — it triggers disputes and chargebacks.

Fulfillment confirmations (shipping, delivery, download ready) reassure the customer that something is moving. Broken tracking links or a mismatched order reference are easy to miss without an automated check.

The common thread: these emails are often the only direct communication your system sends after a user takes a high-intent action. If they're wrong, delayed, or missing, customers notice. Your support queue does too. That's why testing them as part of your CI pipeline, not just manually before launch, matters.

Why email testing is harder than UI testing

Most end-to-end tests follow a predictable pattern: trigger an action, assert what changed in the UI, and move on. Email doesn't work that way.

Email is asynchronous: When a user completes checkout, the confirmation email isn't sent instantaneously. It goes through your app's email service, a third-party provider like SendGrid, and then delivery infrastructure you don't control. You can't assert an inbox the same way you assert a DOM element. You have to wait, poll, and handle cases where the email is delayed or never arrives.

Email delivery can fail silently: Your app might think it sent the email successfully. The API call returned 200; the job was queued. But the customer never received anything. A misconfigured template, a broken queue worker, or a provider-side issue won't throw an error that your UI tests will catch. The only way to know the email arrived is to actually check for it.

Real inboxes don't work well in CI: Gmail and Outlook weren't built for automated testing. They bring OAuth token management, rate limits, and abuse detection that make them unreliable in CI environments, especially during parallel test runs where multiple tests are hitting the same inbox at the same time.

Dynamic content makes assertions fragile: Order confirmation emails aren't static. They contain order IDs, totals, item names, and personalised details that change with every test run. A simple string match won't cut it. You need to extract values and compare them against a known source of truth.

These aren't reasons to skip email testing. They're exactly why it needs its own tooling and strategy, which is what the rest of this guide covers.

What you need before you start

Before writing any tests, make sure you have these three things in place.

A working Cypress setup: You should already have Cypress configured to run against your test or staging environment. If you're starting from scratch, the official Cypress documentation is the right place to begin. This guide assumes you're past that point.

A testmail.app account: You need a dedicated testing inbox to capture emails programmatically in CI. As covered above, real providers like Gmail and Outlook aren't built for this. testmail.app gives you a unique namespace, unlimited tags for inbox isolation, and a simple API that works cleanly inside Cypress. Sign up here.

A non-production environment: Run these tests against a staging or test environment, especially when real financial transactions are involved. Most teams use a sandbox payment provider (Stripe, Braintree, and others all offer them) and seeded test data to keep the flow realistic without touching live systems.

Automating the order confirmation flow with Cypress + testmail.app

At a high level, you're validating a simple contract: a successful order should result in the correct confirmation email.

Trigger a successful order

How you do this depends on your test strategy. Some teams run a small number of true UI checkout flows. Others create the order via API for speed, use a sandbox payment provider, or trigger the confirmation event directly in staging. Each approach tests a different slice of the stack. Pick based on how much of the real flow you need coverage on.

Capture the email

Once the order succeeds, query the testmail.app API for the confirmation email sent to your test inbox. A unique tag per test run keeps emails isolated across parallel CI runs. A timestamp filter ensures you're only looking at emails from the current run. Live query mode holds the connection open until an email arrives. Without it, you risk querying before delivery and getting an empty result.

Extract and verify dynamic data

Order confirmation emails contain values that actually matter: order ID, totals, taxes, item names, and billing details. Parse the email content and compare those values against a trusted source like the API response that created the order, seeded test data, or the success page. This is where subtle but high-impact bugs surface. Formatting issues, mismatched references, and missing line items are easy to miss without an explicit assertion.

If the email includes links like "View order" or "Download invoice," extract the URL and visit it with Cypress. Confirm it resolves correctly and displays the expected content. This catches broken routes, invalid token generation, and environment-specific issues that unit tests won't expose.

Example: Order confirmation email test in Cypress

/**
 * Required environment variables:
 * TESTMAIL_APIKEY
 * TESTMAIL_NAMESPACE
 */

const axios = require('axios').default;
const ChanceJS = require('chance');
const chance = new ChanceJS();

// Build the testmail.app API endpoint using your credentials
const ENDPOINT = `https://api.testmail.app/api/json?apikey=${Cypress.env(
  'TESTMAIL_APIKEY'
)}&namespace=${Cypress.env('TESTMAIL_NAMESPACE')}`;

// Generate a unique tag for this test run.
// Without this, parallel CI runs can pick up each other's emails.
const TAG = chance.string({
  length: 12,
  pool: 'abcdefghijklmnopqrstuvwxyz0123456789'
});

// testmail.app routes any email sent to [email protected]
// to your account — no inbox registration needed.
const TESTEMAIL = `${Cypress.env(
  'TESTMAIL_NAMESPACE'
)}.${TAG}@inbox.testmail.app`;

// Filter to emails from this run only.
// Without this, a leftover email from a previous run could cause a false pass.
const startTimestamp = Date.now();

context('Order Confirmation Email Flow', () => {

  before(() => {
    // Trigger your order here. Options:
    // - cy.task('createTestOrder') — API-based, recommended for most teams
    // - Full UI checkout — more realistic, slower
    // - Direct event trigger — fastest, tests less of the stack
    cy.task('createTestOrder', { email: TESTEMAIL });
  });

  context('Verify order confirmation email', () => {
    let inbox;

    before(done => {
      cy.intercept({
        url: 'https://api.testmail.app/api/json'
      }).as('getEmail');

      axios
        .get(
          // livequery=true holds the connection open until an email arrives.
          // Without it, an empty inbox returns immediately and the test fails.
          `${ENDPOINT}&tag=${TAG}&timestamp_from=${startTimestamp}&livequery=true`
        )
        .then(response => {
          inbox = response.data;
          done();
        })
        .catch(err => done(err));

      cy.wait('@getEmail')
        .its('response.statusCode')
        .should('be.oneOf', [200, 307])
        .then(statusCode => {
          if (statusCode === 307) {
            // 307 means the live query timed out before an email arrived.
            // Usually a delivery delay — but if it happens consistently,
            // check your email service logs.
            cy.wait(5000);
          }
        });
    });

    it('Email request should succeed', () => {
      expect(inbox.result).to.equal('success');
    });

    it('Should receive one order confirmation email', () => {
      // If count > 1, you may have a duplicate send bug worth investigating.
      expect(inbox.count).to.equal(1);
    });

    it('Should validate subject and basic structure', () => {
      const email = inbox.emails[0];
      expect(email.subject).to.include('Order Confirmation');
      expect(email.html).to.contain('Thank you for your order');
    });

    it('Should extract and validate dynamic order data', () => {
      const email = inbox.emails[0];

      const orderIdMatch = email.html.match(/Order\s?#(\d+)/);
      expect(orderIdMatch, 'Order ID not found').to.not.be.null;

      const totalMatch = email.html.match(/\$\d+\.\d{2}/);
      expect(totalMatch, 'Total amount not found').to.not.be.null;

      // Where possible, assert against the actual order object
      // rather than just checking existence:
      // expect(orderIdMatch[1]).to.equal(createdOrder.id);
      // expect(totalMatch[0]).to.equal(createdOrder.formattedTotal);
    });

    it('Should validate the View Order link', () => {
      const email = inbox.emails[0];

      const linkMatch = email.html.match(/https?:\/\/[^\s"]*order[^\s"]*/);
      expect(linkMatch, 'Order link not found').to.not.be.null;

      const orderLink = linkMatch[0];
      expect(orderLink).to.include('/order');

      cy.request(orderLink).its('status').should('eq', 200);
    });

  });
});

Additional scenarios worth testing

The core test covers the happy path. These scenarios go further — validating correctness, consistency, and the kinds of failures that only show up in production.

Duplicate submissions

If a user double-clicks "Pay" or retries during a slow checkout, your system should not create multiple orders or send multiple confirmation emails for the same transaction. Trigger the order flow twice in quick succession and assert that only one order is persisted and only one email is sent. This validates idempotency at the payment and order-processing layer — something that's easy to break during refactors and hard to catch without an explicit test.

Order succeeds, but email delivery breaks

Payment processing, order persistence, and email delivery are usually separate services or background jobs. It's possible for an order to be marked successful while email delivery silently fails — due to a queue issue, a misconfiguration, or a template error. Your test should assert that a successfully created order always results in a confirmation email. If the order exists but no email arrives, that's a production-impacting failure most test suites won't catch.

Lifecycle follow-ups

If your system sends follow-up emails — refunds, cancellations, shipping updates — they should reference the same order ID and reflect accurate state. Tests can validate that these emails correctly map to the original transaction and display the right information at each stage. A refund email that shows the original charge amount, or a cancellation that references the wrong order, are the kinds of bugs that erode customer trust quietly over time.

Frequently Asked Questions

Can Cypress test emails?

Not natively. Cypress handles the browser side — driving the checkout flow and asserting the UI. To test emails, you need a dedicated inbox API like testmail.app that captures emails and exposes them programmatically, so Cypress can query and assert against them as part of the same test.

Why not use Gmail for email testing in CI?

Gmail wasn't built for automated testing. It requires OAuth token management, enforces rate limits, and flags automated access as suspicious. In parallel CI runs, a shared inbox also means tests can pick up each other's emails. A dedicated testing inbox avoids all of this with no setup overhead.

How do I prevent emails from one test run affecting another?

Use a unique tag per test run and a timestamp filter. The tag isolates your inbox so parallel runs don't interfere with each other. The timestamp ensures you're not picking up leftover emails from previous runs.

What if the test times out waiting for an email?

Check whether it's consistent or intermittent. A one-off timeout is usually a delivery delay. If it happens repeatedly, the order is likely succeeding, but the email job is silently failing — check your email service logs. This is exactly the kind of bug end-to-end email tests are designed to catch.

Subscribe to blog

Stay updated with our latest insights and curated articles delivered straight to your inbox.