# Email Validation



Two ways to validate email addresses against Surface's provider waterfall and the environment's spam blacklist. Both share one per-address verdict cache, so an address is billed once.

## What happens on an invalid address is a policy decision [#what-happens-on-an-invalid-address-is-a-policy-decision]

The form owner decides:

| Policy                      | Behavior                                                                      | Implementation                                         |
| --------------------------- | ----------------------------------------------------------------------------- | ------------------------------------------------------ |
| **Annotate only** (default) | Visitor continues; the response carries the verdict for workflows and scoring | `emailValidation: true` on `init`                      |
| Soft warn                   | Inline note, visitor continues                                                | `validateEmail` in your handler, message on `!isValid` |
| Block                       | Visitor stays on the step until deliverable                                   | `validateEmail` gate before `next()`                   |
| Disqualify                  | End on the "not a fit" ending                                                 | `validateEmail`, then `disqualify()`                   |

Never gate navigation on validation unless the owner explicitly chose block or disqualify.

## Automatic mode [#automatic-mode]

```js
SurfaceForms.init({ ..., emailValidation: true });
```

Validates email answers as captured (debounced) and writes the same verdict keys native forms write (`isEmailValid`, `emailValidation`, `emailValidationFailed`) to the response's metadata. Never blocks navigation or submission. Off by default because verdicts bill the owner's validator quota.

## On-demand checks [#on-demand-checks]

`form.validateEmail(email)` resolves the server-side verdict `{ isValid, status, reason, failed? }`:

```js
// Block: keep the visitor on the step with a message.
continueButton.addEventListener("click", async () => {
  const { isValid, reason, failed } = await form.validateEmail(emailInput.value);
  if (isValid || failed) return form.next(); // fail open: never lock out on a vendor outage
  errorEl.textContent = `That address looks undeliverable (${reason}).`;
});
```

For the disqualify policy, call `form.disqualify()` instead of showing the message. Pages with a gate should carry `data-surface-nav="js"` so `validate_form_html` knows navigation is code-driven. Pairing a gate with `emailValidation: true` is fine; the shared cache bills once.

## Verdicts fail open [#verdicts-fail-open]

When no validator can run, the result is `{ isValid: true, failed: true, status: "unknown" }`: an outage must never lock a visitor out. Check `failed` to soft-warn instead of hard-block, and expect an address accepted today (fail open) to return a definitive `invalid` tomorrow as the waterfall resolves.

In [preview mode](/docs/forms-sdk/resume-and-preview), validation resolves synthetic valid verdicts with no network and no billing. `form.capabilities.emailValidation` reports whether automatic mode is active.
