Check an email before a signup form continues

Check email syntax and domain signals, offer an optional spelling correction, and keep a late response from approving an edited address.

JavaScript (browser)Last tested Get the complete example ↓
On this page

Working example

HTML + JavaScript

Before you start

Build an email step with Check email and Continue buttons. The first button checks the current address. The second demonstrates where your signup flow would continue, without creating an account or sending a message.

Create a Public key on the keys page, and allow localhost plus your production hostname. Public keys start with parse_public_. Replace YOUR_PUBLIC_API_KEY in the complete HTML example with that key, then serve the downloaded file locally. Keep secret keys on your server.

From the directory containing check-email-before-submit.html, run:

Terminal
python3 -m http.server 8000

Open the local example. Use localhost, matching the allowed hostname on your key. Opening the file directly with file:// does not supply the website origin the public key needs.

The browser check is feedback for the person filling out the form. Your server must still validate submitted data and apply any signup rules. Browser controls can be bypassed.

1. Keep checking separate from typing

Use an email input with a visible label and the browser's built-in format check. The example only requests the API after Check email is clicked. Typing clears the previous verdict and disables Continue until the new value has been checked.

HTML
<form id="email-form" aria-busy="false">
  <label for="email">Email address
    <input id="email" name="email" type="email" autocomplete="email" spellcheck="false"
      autocapitalize="none" maxlength="254" value="hello@gmail.com" required>
  </label>
  <div class="actions">
    <button type="submit">Check email</button>
    <button id="suggestion" class="secondary" type="button" hidden></button>
  </div>
  <p id="status" role="status" aria-live="polite" aria-atomic="true" data-error="false">Nothing is checked until you ask.</p>
  <div id="details" class="result" hidden></div>
  <button id="continue" class="secondary" type="button" disabled>Continue</button>
  <output id="next" aria-live="polite"></output>
</form>

This avoids a request for every keystroke and makes it clear which address the result describes. The input remains editable while the request is in flight. Editing cancels that work, so an answer for an old address cannot approve a new one.

2. Check the core email response

Call /email/{email} with the encoded address. This workflow uses the core response and does not request deep.

JavaScript
async function checkEmail(value, signal) {
  if (!API_KEY.startsWith('parse_public_')) {
    throw new Error('Add your public API key and allow this hostname in its domain list.');
  }
  const url = new URL(`https://api.parseapi.com/email/${encodeURIComponent(value)}`);
  url.search = new URLSearchParams({ key: API_KEY });
  const response = await fetch(url, { signal });
  if (!response.ok) {
    const messages = {
      400: 'Check the email spelling, including its domain, and try again.',
      401: 'Check your public API key.',
      403: 'Check your public key, allowed hostnames, and account access.',
      429: 'The request limit was reached. Try again later.'
    };
    throw new Error(messages[response.status] || 'The email check is unavailable. Try again later.');
  }
  return response.json();
}

Check the HTTP status before reading a result. A malformed address can return 400; a rejected key or allowance limit is a failed check, not evidence about the email. The example leaves Continue disabled and explains the failure beside the form.

A ten-second timeout keeps the form from waiting indefinitely. A request revision check protects the UI even if a superseded response arrives after cancellation.

3. Turn separate signals into clear feedback

The response answers several different questions:

  • valid describes the address's format.
  • domain_valid describes the domain's mail configuration. null means unknown.
  • didyoumean offers a possible spelling correction.
  • role and disposable flag address characteristics you may want to consider in your own workflow.
JavaScript
function showResult(data, value) {
  if (!data || typeof data.valid !== 'boolean') {
    throw new Error('The email check returned an unexpected response. Try again.');
  }
  const lines = [
    `Format: ${data.valid ? 'valid' : 'invalid'}.`,
    `Mail domain: ${data.domain_valid === true ? 'configured' : data.domain_valid === false ? 'not configured' : 'unknown'}.`
  ];
  if (data.role === true) lines.push('This looks like a shared or role address.');
  if (data.disposable === true) lines.push('This domain is flagged as disposable.');
  details.replaceChildren(...lines.map((text) => {
    const line = document.createElement('p');
    line.textContent = text;
    return line;
  }));
  details.hidden = false;
  if (typeof data.didyoumean === 'string' && data.didyoumean !== value) {
    suggestedEmail = data.didyoumean;
    suggestion.textContent = `Use ${suggestedEmail}`;
    suggestion.hidden = false;
  }
  // This demo blocks a known format/domain failure, and keeps unknown visible.
  const ready = data.valid && data.domain_valid !== false;
  checkedEmail = ready ? value : null;
  continueButton.disabled = !ready;
  setStatus(ready
    ? 'Check complete. Review the details before continuing.'
    : 'Review the email spelling before continuing.', !ready);
}

This example allows continuation when the format is valid and the mail domain is not explicitly invalid. An unknown domain remains visibly unknown. Role and disposable flags appear as information; a shared mailbox can be a perfectly reasonable contact address.

Treat that rule as a deliberate choice for this demo. If your application needs a different policy, change the server rule and the browser feedback together. Never turn an unknown result into a claim that an inbox exists.

4. Offer corrections without replacing input

When didyoumean has a value, show a button with the proposed address. The email field only changes after the person chooses it. Applying the suggestion clears the old result and requires another check.

Try hello@gmial.com to exercise this branch. A suggestion is not proof that the original address is wrong or that the suggested mailbox belongs to the person. Keep the original address available until they accept the change.

The demo's Continue button checks that the current input still matches the successfully checked value. It then shows a local confirmation. Replace that final action with your own form submission when integrating the example.

Try the failure paths

Check the default address, then change one character and confirm Continue disables. Start another check and edit the input immediately; the late result should never appear. Apply a spelling suggestion and confirm it requires a fresh check.

Use your browser's Network panel to try the form offline. The message should explain the failed request while keeping the address editable. A key rejection or rate limit should also leave the form in an honest unchecked state.

What the check tells you

A valid format and configured mail domain do not prove that the mailbox accepts mail or that the person controls it. The Email API reference explains the response fields and the separate deliverability check. To establish ownership in a signup flow, send a confirmation link through your own email system and require the person to use it.

Once the email step works, normalize the phone numbers your application collects. The Email API provides another live example when you want to inspect the full response.

Complete example

One HTML file with the form, styles, and JavaScript. Replace YOUR_PUBLIC_API_KEY with your public key, then run it on a domain you have added to that key.

Download HTML
View and copy the complete source
check-email-before-submit.html
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Check an email before continuing</title>
  <style>
    :root { color-scheme: light dark; --bg: #fff; --ink: #152029; --muted: #5e6b75; --line: #d9e2e7; --field: #f6f9fa; --accent: #007c91; --button: #c5f6ff; --error: #b42335; }
    @media (prefers-color-scheme: dark) {
      :root:not([data-theme="light"]) { --bg: #0b1015; --ink: #eff7fa; --muted: #94a6b2; --line: #2c3942; --field: #121b23; --accent: #67e8f9; --button: #67e8f9; --error: #fda4af; }
    }
    :root[data-theme="dark"] { color-scheme: dark; --bg: #0b1015; --ink: #eff7fa; --muted: #94a6b2; --line: #2c3942; --field: #121b23; --accent: #67e8f9; --button: #67e8f9; --error: #fda4af; }
    :root[data-theme="light"] { color-scheme: light; }
    * { box-sizing: border-box; }
    body { margin: 0; padding: 24px; background: var(--bg); color: var(--ink); font: 15px/1.5 system-ui, sans-serif; }
    main { max-width: 540px; margin: 0 auto; }
    h1 { margin: 0 0 6px; font-size: clamp(23px, 5vw, 30px); font-weight: 600; letter-spacing: -.035em; }
    p { margin: 0; }
    .intro, .note { color: var(--muted); }
    form { margin-top: 24px; }
    label { display: block; font-size: 13px; font-weight: 550; }
    input, button { font: inherit; }
    input { width: 100%; min-width: 0; margin-top: 7px; padding: 11px 12px; border: 1px solid var(--line); border-radius: 7px; background: var(--field); color: var(--ink); }
    :focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }
    .lookup { display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: end; gap: 12px; }
    .fields { display: grid; grid-template-columns: minmax(0, 1fr) 90px; gap: 12px; margin-top: 20px; }
    button { min-height: 46px; padding: 11px 16px; border: 0; border-radius: 7px; background: var(--button); color: #07313c; cursor: pointer; font-size: 14px; font-weight: 650; }
    button:disabled { opacity: .55; cursor: wait; }
    #status { margin-top: 14px; min-height: 44px; color: var(--muted); font-size: 13px; }
    #status[data-error="true"] { color: var(--error); }
    .note { border-top: 1px solid var(--line); margin-top: 12px; padding-top: 16px; font-size: 12px; }
    @media (max-width: 380px) { body { padding: 18px; } .lookup { grid-template-columns: 1fr; } button { width: 100%; } }

    [hidden] { display: none !important; }
    .actions > button { max-width: 100%; overflow-wrap: anywhere; }
    .actions { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 16px; }
    .secondary { background: var(--field); color: var(--ink); border: 1px solid var(--line); }
    .result { margin-top: 16px; font-size: 13px; }
    .result p + p { margin-top: 6px; }
    output { overflow-wrap: anywhere; display: block; margin-top: 14px; font-size: 13px; color: var(--accent); }
  </style>
</head>
<body>
  <main>
    <h1>Catch the easy mistakes.</h1>
    <p class="intro">Check an email before continuing. You choose whether to accept a suggested spelling.</p>
    <!-- example:form -->
    <form id="email-form" aria-busy="false">
      <label for="email">Email address
        <input id="email" name="email" type="email" autocomplete="email" spellcheck="false"
          autocapitalize="none" maxlength="254" value="hello@gmail.com" required>
      </label>
      <div class="actions">
        <button type="submit">Check email</button>
        <button id="suggestion" class="secondary" type="button" hidden></button>
      </div>
      <p id="status" role="status" aria-live="polite" aria-atomic="true" data-error="false">Nothing is checked until you ask.</p>
      <div id="details" class="result" hidden></div>
      <button id="continue" class="secondary" type="button" disabled>Continue</button>
      <output id="next" aria-live="polite"></output>
    </form>
    <!-- /example:form -->
    <p class="note">This demo checks email syntax and domain signals. It does not send email, create an account, or prove mailbox ownership.</p>
  </main>
  <script>
    // Restrict this public key to your development and production hostnames.
    const API_KEY = 'YOUR_PUBLIC_API_KEY';
    const form = document.querySelector('form');
    const button = form.querySelector('button[type="submit"]');
    const status = document.querySelector('#status');
    let controller;
    let revision = 0;

    function setStatus(message, error = false) {
      status.textContent = message;
      status.dataset.error = String(error);
    }

    function cancelRequest() {
      revision += 1;
      controller?.abort();
      controller = undefined;
      button.disabled = false;
      form.setAttribute('aria-busy', 'false');
    }

    const email = document.querySelector('#email');
    const suggestion = document.querySelector('#suggestion');
    const details = document.querySelector('#details');
    const continueButton = document.querySelector('#continue');
    const next = document.querySelector('#next');
    let checkedEmail = null;
    let suggestedEmail = null;

    function clearResult() {
      checkedEmail = null;
      suggestedEmail = null;
      suggestion.hidden = true;
      continueButton.disabled = true;
      details.hidden = true;
      details.replaceChildren();
      next.textContent = '';
    }

    // example:request
    async function checkEmail(value, signal) {
      if (!API_KEY.startsWith('parse_public_')) {
        throw new Error('Add your public API key and allow this hostname in its domain list.');
      }
      const url = new URL(`https://api.parseapi.com/email/${encodeURIComponent(value)}`);
      url.search = new URLSearchParams({ key: API_KEY });
      const response = await fetch(url, { signal });
      if (!response.ok) {
        const messages = {
          400: 'Check the email spelling, including its domain, and try again.',
          401: 'Check your public API key.',
          403: 'Check your public key, allowed hostnames, and account access.',
          429: 'The request limit was reached. Try again later.'
        };
        throw new Error(messages[response.status] || 'The email check is unavailable. Try again later.');
      }
      return response.json();
    }
    // /example:request

    // example:result
    function showResult(data, value) {
      if (!data || typeof data.valid !== 'boolean') {
        throw new Error('The email check returned an unexpected response. Try again.');
      }
      const lines = [
        `Format: ${data.valid ? 'valid' : 'invalid'}.`,
        `Mail domain: ${data.domain_valid === true ? 'configured' : data.domain_valid === false ? 'not configured' : 'unknown'}.`
      ];
      if (data.role === true) lines.push('This looks like a shared or role address.');
      if (data.disposable === true) lines.push('This domain is flagged as disposable.');
      details.replaceChildren(...lines.map((text) => {
        const line = document.createElement('p');
        line.textContent = text;
        return line;
      }));
      details.hidden = false;
      if (typeof data.didyoumean === 'string' && data.didyoumean !== value) {
        suggestedEmail = data.didyoumean;
        suggestion.textContent = `Use ${suggestedEmail}`;
        suggestion.hidden = false;
      }
      // This demo blocks a known format/domain failure, and keeps unknown visible.
      const ready = data.valid && data.domain_valid !== false;
      checkedEmail = ready ? value : null;
      continueButton.disabled = !ready;
      setStatus(ready
        ? 'Check complete. Review the details before continuing.'
        : 'Review the email spelling before continuing.', !ready);
    }
    // /example:result

    email.addEventListener('input', () => {
      cancelRequest();
      clearResult();
      setStatus('Check this email before continuing.');
    });

    suggestion.addEventListener('click', () => {
      if (!suggestedEmail) return;
      email.value = suggestedEmail;
      cancelRequest();
      clearResult();
      setStatus('Suggested spelling applied. Check the new email before continuing.');
      email.focus();
    });

    continueButton.addEventListener('click', () => {
      if (checkedEmail === null || checkedEmail !== email.value.trim()) return;
      next.textContent = `Ready to continue with ${checkedEmail}. This demo stops here.`;
    });

    form.addEventListener('submit', async (event) => {
      event.preventDefault();
      if (!form.reportValidity()) return;
      cancelRequest();
      clearResult();
      const value = email.value.trim();
      const current = revision;
      controller = new AbortController();
      const request = controller;
      let timedOut = false;
      const timeout = setTimeout(() => { timedOut = true; request.abort(); }, 10000);
      button.disabled = true;
      form.setAttribute('aria-busy', 'true');
      setStatus('Checking email...');
      try {
        const data = await checkEmail(value, request.signal);
        if (current !== revision) return;
        showResult(data, value);
      } catch (error) {
        if (current !== revision) return;
        setStatus(timedOut ? 'The email check timed out. Try again.'
          : error instanceof TypeError ? 'Could not reach parseAPI. Check your connection.'
          : error instanceof SyntaxError ? 'The response could not be read. Try again.'
          : error.message, true);
      } finally {
        clearTimeout(timeout);
        if (current === revision) cancelRequest();
      }
    });
  </script>
</body>
</html>

Need a key? Create a public API key. For every field and parameter, see Email API reference.