# Validate emails on sign-up forms

URL: https://email-validation.org/docs/guides/signup-form-validation/
Updated: 2026-10-20

> Documentation index: https://email-validation.org/llms.txt

Validating addresses at sign-up catches typos and throwaway inboxes before they reach your database, but a check
must never stop a real person from signing up. The rule of thumb: **block only what is clearly wrong, accept
everything else, and re-check what was unclear later.**

## Call the API from your server

Validate in your sign-up handler on the server, not in browser code: your API key stays secret, and bots cannot
spend your credits by calling the API directly. If you also want feedback while the person types, call your own
endpoint, add a bot check (such as a CAPTCHA or Turnstile) and rate-limit it.

Validate once, when the form is submitted, rather than on every keystroke: each completed check costs a credit.

## Keep it fast

Send `"timeout": 6` (or anything from 5 to 15) so slow mail servers do not hold up
the form. A check that runs out of time is `unknown` and free, and you can re-check it later.

## Decide on the result

| Result | Suggested handling |
|---|---|
| `deliverable` | Accept. |
| `undeliverable` | Ask the person to fix the address. Offer `did_you_mean` when it is set. |
| `risky` with `disposable` | Ask for a permanent address, if your product needs one. |
| `risky` with another reason | Accept and flag (for example for a confirmation email or review). |
| `unknown` | Accept and re-check later. |
| Error `402`, `429` or `503`, or no answer | Treat like `unknown`: accept and re-check later. |

**JavaScript**

```js
// Returns { ok, message?, suggestion? } for your sign-up handler.
export async function screenSignupEmail(email, ip) {
  let res;
  try {
    res = await fetch('https://api.email-validation.org/v1/validate', {
      method: 'POST',
      headers: {
        Authorization: `Bearer ${process.env.EV_API_KEY}`,
        'Content-Type': 'application/json',
      },
      // A short budget keeps the form fast; a check that runs out of time is unknown and free.
      body: JSON.stringify({ email, ip_address: ip, timeout: 6 }),
      signal: AbortSignal.timeout(10_000),
    });
  } catch {
    return { ok: true, recheck: true }; // network problem: accept, check later
  }
  if (!res.ok) return { ok: true, recheck: true }; // 402, 429, 503 …: treat like unknown

  const r = await res.json();
  if (r.status === 'undeliverable') {
    return {
      ok: false,
      message: r.did_you_mean ? `Did you mean ${r.did_you_mean}?` : 'Please check this email address.',
      suggestion: r.did_you_mean,
    };
  }
  if (r.status === 'risky' && r.reason === 'disposable') {
    return { ok: false, message: 'Please use a permanent email address.' };
  }
  // deliverable, other risky reasons, unknown: accept (flag risky and unknown for review or a later re-check)
  return { ok: true, flag: r.status === 'risky' || r.status === 'unknown' };
}
```

**Python**

```python
import os

import requests

API = "https://api.email-validation.org/v1/validate"


def screen_signup_email(email: str, ip: str | None = None) -> dict:
    """Returns {"ok": bool, "message": str | None}. Never blocks on our errors."""
    payload = {"email": email, "timeout": 6}
    if ip:
        payload["ip_address"] = ip
    try:
        resp = requests.post(
            API,
            headers={"Authorization": f"Bearer {os.environ['EV_API_KEY']}"},
            json=payload,
            timeout=10,
        )
    except requests.RequestException:
        return {"ok": True, "recheck": True}
    if not resp.ok:  # 402, 429, 503 …: treat like unknown
        return {"ok": True, "recheck": True}

    r = resp.json()
    if r["status"] == "undeliverable":
        hint = f"Did you mean {r['did_you_mean']}?" if r["did_you_mean"] else "Please check this email address."
        return {"ok": False, "message": hint}
    if r["status"] == "risky" and r["reason"] == "disposable":
        return {"ok": False, "message": "Please use a permanent email address."}
    return {"ok": True, "flag": r["status"] in ("risky", "unknown")}
```

## Words that help

- Likely typo: "Did you mean **name@gmail.com**?" with a button that applies the suggestion.
- Undeliverable: "We could not deliver to this address. Please check it for typos."
- Disposable: "Please use an email address you will keep: we send your receipts and sign-in codes there."

Avoid messages that reveal how your screening works in detail, and never tell the person that "our provider" is
down: just let them through.

## Afterwards

- Re-check addresses flagged `unknown` with a [bulk job](https://email-validation.org/docs/api/bulk/) or later single checks.
- A confirmation email (double opt-in) remains the only way to prove the person controls the mailbox; validation
  makes sure the message can arrive.
- Watch your bounce and complaint rates per sign-up source to tune what you accept.

## Related

- [Errors and retries](https://email-validation.org/docs/errors/#signup-forms)
- [The risky status](https://email-validation.org/docs/statuses/risky/) and [the unknown status](https://email-validation.org/docs/statuses/unknown/)
- [Test addresses](https://email-validation.org/docs/test-addresses/) to try every branch of your handler for free
