splitforms.com
All articles/ GUIDES11 MIN READPublished May 8, 2026

How to Test Form Submissions Without Sending Real Emails

Test contact form submissions without spamming your inbox — dev keys, Mailtrap, request bins, and a copy-paste Playwright recipe that skips real email.

✶ Written by
splitforms.com / blog

Founder of splitforms — the form backend API for developers. Writes about form UX, anti-spam, and shipping web apps without backend code.

Why "just submit it and check the inbox" is a bad pattern

You're building a contact form. To test the form submission, you fill it in with fake data and click submit. The notification email lands in your inbox. You delete it. Repeat 30 times during the dev cycle. By Friday afternoon, your inbox has 117 emails titled "Test test test" and you've missed two real customer messages buried in the noise.

The fix is a discipline: never test against the same destination as production. Use a separate access key during development, a separate notification email (or no email at all), and an end-to-end test that asserts on the HTTP response — not on whether an email arrived. The patterns below cover the four common test scenarios: local development, manual QA, automated end-to-end tests, and load testing.

Pattern 1: a dev access key with a sandbox destination

The cleanest pattern is two access keys: production (configured to email you) and dev (configured to email a Mailtrap inbox or to forward to a webhook bin). The form's code reads the key from an env var, so the key swaps automatically per environment.

<!-- Dev / staging form posts to a sandbox key -->
<form action="https://splitforms.com/api/submit" method="POST">
  <input
    type="hidden"
    name="access_key"
    value="${process.env.NEXT_PUBLIC_SPLITFORMS_DEV_KEY}"
  >
  <input name="name" required>
  <input name="email" type="email" required>
  <textarea name="message" required></textarea>
  <button type="submit">Send</button>
</form>

Inside the splitforms dashboard, configure the dev access key's notification email to point at a Mailtrap inbox (or any sandbox catch-all). Submissions land in Mailtrap, you can inspect them, and your real inbox stays clean. Webhooks on the dev key forward to a request-bin URL that captures payloads for end-to-end tests. See /docs for the access-key setup walkthrough.

Pattern 2: verify the endpoint contract with a real submission

splitforms doesn't have a "test mode" that skips email or skips your quota — a test submission through /api/submitis a real submission. It lands in your dashboard, counts toward your monthly limit, and (if notifications are on) sends a real email. There's no way to fake that from the API side, so don't architect around one existing.

What splitforms does give you is a built-in test-submission harness in the dashboard (Form → "Send a test submission"): it fills your access key automatically and fires a real POST at /api/submit so you can confirm field names, the redirect, and the notification email are wired correctly before going live. Treat its output like production traffic — because it is.

For anything you run more than a couple of times (dev loop, CI), that's exactly why Pattern 1's separate dev access keymatters: it's the only supported way to keep test volume off your production quota and out of your real inbox. Confirm the contract once with curl, then stop hitting the live endpoint for routine testing:

# One-time sanity check against a DEV access key (not production)
curl -X POST https://splitforms.com/api/submit \
  -H "Accept: application/json" \
  --data-urlencode "access_key=YOUR_DEV_KEY" \
  --data-urlencode "name=Test User" \
  --data-urlencode "email=test@example.com" \
  --data-urlencode "message=Hello"

# Response:
# {"success":true,"submission_id":"sub_xxxx","message":"Submission accepted"}

This confirms the request shape works. From here, everyday testing should go through the dev key's own sandboxed notification address (Pattern 1) or a stubbed network call (Pattern 5) — not repeated hits on the live endpoint.

Pattern 3: local request bin for development

For pure local-only testing where you don't want any external service involved, point the form's action at a local request-bin. Two good options:

  • RequestBin (self-hosted). Run a Docker container locally on port 3001. The form posts to http://localhost:3001/bin/abc, the bin captures the request, you inspect the payload in the web UI.
  • HTTPie one-liner. http --pretty=format -p HhBb POST :3001 — listens on port 3001, prints every incoming request to stdout.
  • The dev console approach. Open the browser's Network tab, submit the form, inspect the request payload in the Network tab without it ever needing to hit a backend. Add a preventDefault() on submit so the form doesn't actually navigate.
<!-- Point the form at a local request bin -->
<form action="http://localhost:3001/bin/abc" method="POST">
  <input name="name" required>
  <input name="email" type="email" required>
  <textarea name="message" required></textarea>
  <button type="submit">Send</button>
</form>

<!-- Or intercept the submit and just log the FormData -->
<script>
  document.querySelector('form').addEventListener('submit', (e) => {
    e.preventDefault();
    const data = Object.fromEntries(new FormData(e.target));
    console.log('FORM SUBMIT', data);
  });
</script>

This pattern is fast to set up but only verifies the client-side payload — it doesn't exercise the real backend, so the spam filter and the access-key validation aren't tested. Use it during initial form-component development; switch to the dev-key pattern once the form is structurally complete.

Pattern 4: Mailtrap (or any sandbox SMTP)

When you want to test the email itself — subject line, reply-to header, formatting — point the dev access key at a Mailtrap inbox. Mailtrap captures emails in a fake inbox you can inspect via web UI, REST API or webhook. The same applies to other sandbox SMTPs: MailHog, Ethereal, your own catch-all SES configuration set.

Configure the destination in the splitforms dashboard:

  1. Sign up at mailtrap.io and copy the inbox address (looks like abc123@inbox.mailtrap.io).
  2. In the splitforms form settings, set the dev access key's notification email to that Mailtrap address.
  3. Submit a test form. Within seconds, the email appears in the Mailtrap inbox. Click into it to inspect headers, reply-to, body formatting.

Mailtrap's free tier covers most dev usage (500 emails/month). For CI runs that send hundreds of test submissions per build, prefer the stubbed-network pattern (Pattern 5, below) so the run never touches splitforms — or your dev key's quota — at all.

Pattern 5: Playwright with stubbed network

For end-to-end tests, the right pattern is to stub the network call so the form never actually hits splitforms. Playwright's page.route() intercepts the POST and returns a fake response:

// tests/contact.spec.ts
import { test, expect } from '@playwright/test';

test('contact form submits and shows success state', async ({ page }) => {
  // Intercept the splitforms POST and return a fake success response.
  await page.route('https://splitforms.com/api/submit', async (route) => {
    const request = route.request();
    expect(request.method()).toBe('POST');

    const body = request.postData() ?? '';
    expect(body).toContain('access_key=');
    expect(body).toContain('email=alice%40example.com');

    await route.fulfill({
      status: 200,
      contentType: 'application/json',
      body: JSON.stringify({
        success: true,
        submission_id: 'sub_test',
        message: 'Submission accepted',
      }),
    });
  });

  await page.goto('http://localhost:3000/contact');
  await page.fill('input[name="name"]', 'Alice');
  await page.fill('input[name="email"]', 'alice@example.com');
  await page.fill('textarea[name="message"]', 'Hello world');
  await page.click('button[type="submit"]');

  await expect(page.getByRole('status')).toHaveText(/thanks/i);
});

test('contact form shows error on submission failure', async ({ page }) => {
  await page.route('https://splitforms.com/api/submit', (route) => {
    route.fulfill({
      status: 500,
      contentType: 'application/json',
      body: JSON.stringify({ success: false, message: 'Something went wrong' }),
    });
  });

  await page.goto('http://localhost:3000/contact');
  await page.fill('input[name="name"]', 'Alice');
  await page.fill('input[name="email"]', 'alice@example.com');
  await page.fill('textarea[name="message"]', 'Hello world');
  await page.click('button[type="submit"]');

  await expect(page.getByRole('alert')).toBeVisible();
});

This is the right pattern for CI. It runs in seconds, doesn't depend on splitforms being reachable from the test runner, and verifies both happy-path and error-path UI behavior. The Cypress equivalent uses cy.intercept('POST', 'https://splitforms.com/api/submit', fixture).

Pattern 6: curl smoke test for production

When you want to verify production end-to-end (real key, real email destination), do it once with a deliberate test submission marked as such:

curl -X POST https://splitforms.com/api/submit \
  -H "Accept: application/json" \
  --data-urlencode "access_key=PROD_KEY" \
  --data-urlencode "name=PROD SMOKE TEST" \
  --data-urlencode "email=qa+formsmoke@yourdomain.com" \
  --data-urlencode "message=production deploy verification at $(date -u +%Y-%m-%dT%H:%M:%SZ)"

The name and message are obviously test data; the email uses a +formsmoke alias so the resulting notification can be auto-archived by an inbox rule. Run this once per deploy from CI to confirm the live form works end-to-end without needing a human to fill it in.

Watch out: CORS errors that only happen in production

The most common "works on localhost, breaks in prod" bug for forms is a CORS error caused by the access key's domain whitelist. splitforms (and most other backends) lock submissions to a configured list of origins. localhost:3000 is whitelisted by default; preview-deployment-abc.vercel.appisn't.

For deeper coverage, see the CORS error form submission fix guide— it's the most-likely follow-up debug after a test passes locally and fails on the preview URL.

Get a dev key

Sign up at /login for a free splitforms account. Generate two access keys — one for prod, one for dev — point each at a different notification email. The free tier covers 500 submissions/month, which is plenty even when test runs are loud. Pricing details at /pricing.

FAQ

Why shouldn't I just submit the form to test it?

Three reasons. (1) Repeated test submissions during development can mark your domain as a spam source. (2) Each test counts against your monthly submission quota — splitforms doesn't have a quota-free test mode, so every POST to /api/submit is a real submission. (3) Real notification emails to your inbox during a test run create noise — you can lose the actual customer submissions in a sea of `test test test`. The right pattern is a separate dev access key with its own sandbox notification address, plus a request-bin or stubbed fetch for automated tests that assert on the response, not on email arrival.

Can I disable email delivery on a form during development?

Not per-submission — splitforms doesn't have a toggle that skips email or quota for individual test submissions; every accepted POST counts and can trigger a notification. The supported way to stop test email hitting your real inbox is a second access key whose notification address points at Mailtrap (or any sandbox inbox) instead of you. Formspree has a deactivate switch, but it returns an error instead of accepting test data — a dev key is the pattern that actually lets you inspect the payload.

What's the simplest local-only test?

Point the form's `action` at a local request-bin running on `localhost:3001` (the `webhook.cookies.dev` CLI tool or `pipedream/requestbin` work well). Submit the form. Inspect the captured payload in the bin's UI to verify field names, encoding and headers. Zero external services involved. The catch: CORS on `localhost` can fool you — see the CORS section below.

How do I test with Playwright or Cypress without spamming real emails?

Use a dev access key bound to a sandbox form, or stub the `fetch` to splitforms with a local mock server. The Playwright recipe in this post shows the route-interception pattern: capture the POST in-test, return a fake success response, and assert on what the form did with that response.

Can I use a real email address that I own for testing?

Yes, but use a dedicated alias (`testforms+dev@yourdomain.com`) and filter it into a folder, so you don't pollute the inbox you read every morning. Some teams set up a shared `formtests@` mailbox the whole team can grep when debugging. This pattern works fine for manual testing; for CI runs, use a request-bin or stubbed fetch instead.

Are there any privacy concerns with testing on production?

Yes — if you submit fake data through a production form, that data lands in the production dashboard and downstream systems (CRM, Slack, webhooks). On most teams that's annoying but harmless; on regulated workloads (healthcare, financial services), test data hitting prod can trip audit controls. Always run end-to-end tests against a separate test environment with its own access key.

What about testing spam filtering?

Use your dev access key, send submissions with the typical spam-bot fingerprint (no time delay, all fields filled, including the honeypot field), and verify they're rejected. The spam filter runs the same way on every access key — dev or production — so a submission that trips it on your dev key would trip it in production too. This does count toward the dev key's quota like any other submission.

About the author
✻ ✻ ✻

Get your free contact form API key in 60 seconds.

500 free form submissions per month. No credit card. No SDK, no PHP, no plugin. Drop one POST endpoint in your form and submissions land in your dashboard and can notify your inbox on every plan.

Generate access key →Read the docs
founders pricing locked in · early access open