Signup form examples

One small web app per SDK language: a signup form that checks the email address with the official SDK before it creates the account. Each one is one source file on the language's standard web stack, with its few dependencies pinned, and each passes the same offline test against a stub of the API.

What every example does

GET / shows a form with one email field. POST /signup validates the address and answers:

ResultAnswer
valid, catch_all, unknownAccepted: "Thanks for signing up!"
invalidRefused, with the reason, and the suggestion when did_you_mean is set
disposableRefused ("Please use an address you will keep"); set BLOCK_DISPOSABLE=false to accept it
No answer in time, rate limited, out of credits or any other API errorAccepted: a check never stops a real person from signing up

The app asks the service to answer within 6 seconds and gives up after 10 (EV_TIMEOUT_MS), without retries. It logs each result's status and reason, never the address, and keeps your key on the server. Settings come from the environment: EV_API_KEY (required), EV_BASE_URL (optional, for example https://api-eu.email-validation.org for EU data residency) and PORT (default 3000). Where to go from here, such as re-checking unknown addresses later, is in the signup form guide.

Try it for free

Sign up with the test addresses: they cost nothing and return a fixed answer, so you can see every branch. accepted@sandbox.email-validation.org is let in, no_mailbox@sandbox.email-validation.org is refused, likely_typo@sandbox.email-validation.org gets a suggestion and disposable@sandbox.email-validation.org is refused as disposable.

JavaScript and TypeScript

Node.js 24 or later, only node:http and the JavaScript SDK. Save the two files in a new folder, then:

npm install
export EV_API_KEY=your_api_key
npm start

Open http://localhost:3000/.

{
  "name": "email-validation-signup-example",
  "version": "1.0.0",
  "private": true,
  "description": "A signup form that validates the email address with the email-validation.org JavaScript SDK.",
  "license": "MIT",
  "type": "module",
  "engines": {
    "node": ">=24"
  },
  "scripts": {
    "start": "node server.js"
  },
  "dependencies": {
    "@email-validation/sdk": "1.1.0"
  }
}
// A signup form that checks the email address with the email-validation.org SDK before it creates the account.
// Run: EV_API_KEY=your_key node server.js, then open http://localhost:3000/
import { createServer } from 'node:http';
import { EmailValidation, EmailValidationError } from '@email-validation/sdk';

const ev = new EmailValidation({
  apiKey: process.env.EV_API_KEY ?? '',
  ...(process.env.EV_BASE_URL ? { baseUrl: process.env.EV_BASE_URL } : {}), // e.g. the EU endpoint
  timeoutMs: Number(process.env.EV_TIMEOUT_MS ?? 10_000), // a few seconds above the 6 s the service may take
  maxRetries: 0, // a signup form does not wait for retries: it lets the person in instead
});
const blockDisposable = process.env.BLOCK_DISPOSABLE !== 'false';

/** The decision for one address: whether to create the account, and what to tell the person. */
async function check(email) {
  if (!email) return { ok: false, message: 'Please enter your email address.' };
  let result;
  try {
    result = await ev.validate(email, { timeout: 6 }); // the service answers within 6 s
  } catch (err) {
    if (!(err instanceof EmailValidationError)) throw err;
    // no answer, rate limited, out of credits …: never block a real person because of it (fail open)
    console.warn(`signup accepted without a check: ${err.code}`);
    return { ok: true, message: 'Thanks for signing up!' };
  }
  console.log(`signup check: ${result.status} (${result.reason})`); // never log the address itself
  if (result.status === 'invalid') {
    const hint = result.did_you_mean ? ` Did you mean ${result.did_you_mean}?` : '';
    return {
      ok: false,
      message: `We could not deliver to this address (${result.reason}). Please check it for typos.${hint}`,
    };
  }
  if (result.status === 'disposable' && blockDisposable) {
    return { ok: false, message: 'Please use an address you will keep, not a disposable one.' };
  }
  // valid, catch_all, unknown (and disposable when allowed): create the account here
  return { ok: true, message: 'Thanks for signing up!' };
}

const escapeHtml = (s) =>
  s.replace(/[&<>"']/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' })[c]);

const page = (message = '') => `<!doctype html>
<html lang="en">
<head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>Sign up</title></head>
<body>
<h1>Sign up</h1>
${message ? `<p>${escapeHtml(message)}</p>` : ''}
<form method="post" action="/signup">
<label>Email <input type="email" name="email" required></label>
<button type="submit">Sign up</button>
</form>
</body>
</html>
`;

const send = (res, status, html) => {
  res.writeHead(status, { 'content-type': 'text/html; charset=utf-8' });
  res.end(html);
};

const server = createServer(async (req, res) => {
  try {
    if (req.method === 'GET' && req.url === '/') return send(res, 200, page());
    if (req.method !== 'POST' || req.url !== '/signup') return send(res, 404, page('Not found.'));
    let body = '';
    for await (const chunk of req) {
      body += chunk;
      if (body.length > 10_000) return send(res, 413, page('That is too long.'));
    }
    const email = (new URLSearchParams(body).get('email') ?? '').trim();
    const { ok, message } = await check(email);
    send(res, ok ? 200 : 422, page(message));
  } catch (err) {
    console.error('signup failed:', err instanceof Error ? err.name : 'error');
    send(res, 500, page('Something went wrong. Please try again.'));
  }
});

const port = Number(process.env.PORT ?? 3000);
server.listen(port, () => console.log(`Listening on http://localhost:${port}/`));

Python

Python 3.11 or later, only the standard library's http.server and the Python SDK. The requirements file pins the SDK and its dependencies with their hashes. Save the two files in a new folder, then:

python3 -m venv .venv && . .venv/bin/activate
pip install --require-hashes -r requirements.txt
EV_API_KEY=your_api_key python app.py

Open http://localhost:3000/.

# The example's dependencies, exact versions with each wheel's sha256: pip install --require-hashes -r requirements.txt
# The SDK and the closure of its one dependency, httpx, for CPython 3.11 and later.
email-validation-sdk==1.1.0 --hash=sha256:fed9456b25af76b3f6e66a971900435c01bc2bec9f9269880302e5093a2d03b2
anyio==4.15.1 --hash=sha256:6152fdbbf9a77fdec97731721bebf7c4c44f7c29b424b0065826173efc7ed101
certifi==2026.10.23 --hash=sha256:e9b54de76e530bb6cf0ccd036787103acb58fd42ba930ea8bf948ddbaa2f61d1
h11==0.16.0 --hash=sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86
httpcore==1.0.9 --hash=sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55
httpx==0.28.1 --hash=sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad
idna==3.20 --hash=sha256:ab7ae7122974553370f0bdb919e1a960b2cd1bc1ef0276416d896db81c14582c
typing_extensions==4.16.0 --hash=sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8
"""A signup form that checks the email address with the email-validation.org SDK before it creates the account.

Run: EV_API_KEY=your_key python app.py, then open http://localhost:3000/
"""

import html
import logging
import os
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from urllib.parse import parse_qs

from email_validation import EmailValidation, EmailValidationError

logging.basicConfig(level=logging.INFO, format="%(message)s")
log = logging.getLogger("signup")

ev = EmailValidation(
    os.environ.get("EV_API_KEY", ""),
    base_url=os.environ.get("EV_BASE_URL") or None,  # e.g. the EU endpoint
    timeout=int(os.environ.get("EV_TIMEOUT_MS", "10000")) / 1000,  # a few seconds above the 6 s the service may take
    max_retries=0,  # a signup form does not wait for retries: it lets the person in instead
)
BLOCK_DISPOSABLE = os.environ.get("BLOCK_DISPOSABLE") != "false"


def check(email: str) -> tuple[bool, str]:
    """The decision for one address: whether to create the account, and what to tell the person."""
    if not email:
        return False, "Please enter your email address."
    try:
        result = ev.validate(email, timeout=6)  # the service answers within 6 s
    except EmailValidationError as err:
        # no answer, rate limited, out of credits ...: never block a real person because of it (fail open)
        log.warning("signup accepted without a check: %s", err.code)
        return True, "Thanks for signing up!"
    log.info("signup check: %s (%s)", result["status"], result["reason"])  # never log the address itself
    if result["status"] == "invalid":
        hint = f" Did you mean {result['did_you_mean']}?" if result["did_you_mean"] else ""
        return False, f"We could not deliver to this address ({result['reason']}). Please check it for typos.{hint}"
    if result["status"] == "disposable" and BLOCK_DISPOSABLE:
        return False, "Please use an address you will keep, not a disposable one."
    # valid, catch_all, unknown (and disposable when allowed): create the account here
    return True, "Thanks for signing up!"


def page(message: str = "") -> bytes:
    note = f"<p>{html.escape(message)}</p>\n" if message else ""
    return f"""<!doctype html>
<html lang="en">
<head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>Sign up</title></head>
<body>
<h1>Sign up</h1>
{note}<form method="post" action="/signup">
<label>Email <input type="email" name="email" required></label>
<button type="submit">Sign up</button>
</form>
</body>
</html>
""".encode()


class Handler(BaseHTTPRequestHandler):
    def send(self, status: int, body: bytes) -> None:
        self.send_response(status)
        self.send_header("Content-Type", "text/html; charset=utf-8")
        self.send_header("Content-Length", str(len(body)))
        self.end_headers()
        self.wfile.write(body)

    def do_GET(self) -> None:
        self.send(200, page()) if self.path == "/" else self.send(404, page("Not found."))

    def do_POST(self) -> None:
        if self.path != "/signup":
            return self.send(404, page("Not found."))
        length = int(self.headers.get("Content-Length") or 0)
        if length > 10_000:
            return self.send(413, page("That is too long."))
        form = parse_qs(self.rfile.read(length).decode("utf-8", "replace"))
        ok, message = check(form.get("email", [""])[0].strip())
        self.send(200 if ok else 422, page(message))


if __name__ == "__main__":
    port = int(os.environ.get("PORT", "3000"))
    server = ThreadingHTTPServer(("", port), Handler)
    log.info("Listening on http://localhost:%d/", port)
    server.serve_forever()

Go

Go 1.22 or later, only net/http and the Go SDK. Save the two files in a new folder, then:

go mod tidy
export EV_API_KEY=your_api_key
go run .

Open http://localhost:3000/.

module signup

go 1.22

require email-validation.org/go/emailvalidation v1.1.0
// A signup form that checks the email address with the email-validation.org SDK before it creates the account.
// Run: EV_API_KEY=your_key go run ., then open http://localhost:3000/
package main

import (
	"context"
	"errors"
	"fmt"
	"html/template"
	"log"
	"net/http"
	"os"
	"strconv"
	"strings"
	"time"

	"email-validation.org/go/emailvalidation"
)

var page = template.Must(template.New("page").Parse(`<!doctype html>
<html lang="en">
<head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>Sign up</title></head>
<body>
<h1>Sign up</h1>
{{if .}}<p>{{.}}</p>
{{end}}<form method="post" action="/signup">
<label>Email <input type="email" name="email" required></label>
<button type="submit">Sign up</button>
</form>
</body>
</html>
`))

type app struct {
	ev              *emailvalidation.Client
	blockDisposable bool
}

// check decides for one address: whether to create the account, and what to tell the person.
func (a *app) check(ctx context.Context, email string) (bool, string) {
	if email == "" {
		return false, "Please enter your email address."
	}
	// the service answers within 6 s
	result, err := a.ev.Validate(ctx, email, &emailvalidation.ValidateOptions{TimeoutSeconds: 6})
	if err != nil {
		// no answer, rate limited, out of credits …: never block a real person because of it (fail open)
		var apiErr *emailvalidation.Error
		if errors.As(err, &apiErr) {
			log.Printf("signup accepted without a check: %s", apiErr.Code)
		}
		return true, "Thanks for signing up!"
	}
	log.Printf("signup check: %s (%s)", result.Status, result.Reason) // never log the address itself
	switch {
	case result.Status == emailvalidation.StatusInvalid:
		hint := ""
		if result.DidYouMean != nil {
			hint = fmt.Sprintf(" Did you mean %s?", *result.DidYouMean)
		}
		return false, fmt.Sprintf("We could not deliver to this address (%s). Please check it for typos.%s", result.Reason, hint)
	case result.Status == emailvalidation.StatusDisposable && a.blockDisposable:
		return false, "Please use an address you will keep, not a disposable one."
	}
	// valid, catch_all, unknown (and disposable when allowed): create the account here
	return true, "Thanks for signing up!"
}

func (a *app) signup(w http.ResponseWriter, r *http.Request) {
	r.Body = http.MaxBytesReader(w, r.Body, 10_000)
	ok, message := a.check(r.Context(), strings.TrimSpace(r.PostFormValue("email")))
	w.Header().Set("Content-Type", "text/html; charset=utf-8")
	if !ok {
		w.WriteHeader(http.StatusUnprocessableEntity)
	}
	_ = page.Execute(w, message)
}

func main() {
	timeoutMs, err := strconv.Atoi(env("EV_TIMEOUT_MS", "10000")) // a few seconds above the 6 s the service may take
	if err != nil {
		log.Fatal("EV_TIMEOUT_MS: not a number")
	}
	opts := []emailvalidation.Option{
		emailvalidation.WithTimeout(time.Duration(timeoutMs) * time.Millisecond),
		emailvalidation.WithMaxRetries(0), // a signup form does not wait for retries: it lets the person in instead
	}
	if base := os.Getenv("EV_BASE_URL"); base != "" {
		opts = append(opts, emailvalidation.WithBaseURL(base)) // e.g. the EU endpoint
	}
	ev, err := emailvalidation.New(os.Getenv("EV_API_KEY"), opts...)
	if err != nil {
		log.Fatal(err)
	}
	a := &app{ev: ev, blockDisposable: os.Getenv("BLOCK_DISPOSABLE") != "false"}

	mux := http.NewServeMux()
	mux.HandleFunc("GET /{$}", func(w http.ResponseWriter, _ *http.Request) {
		w.Header().Set("Content-Type", "text/html; charset=utf-8")
		_ = page.Execute(w, "")
	})
	mux.HandleFunc("POST /signup", a.signup)
	port := env("PORT", "3000")
	log.Printf("Listening on http://localhost:%s/", port)
	log.Fatal(http.ListenAndServe(":"+port, mux))
}

func env(name, fallback string) string {
	if v := os.Getenv(name); v != "" {
		return v
	}
	return fallback
}

PHP

PHP 8.1 or later with ext-curl, PHP's built-in web server and the PHP SDK from Composer. Save the two files in a new folder, then:

composer install
export EV_API_KEY=your_api_key
php -S localhost:3000 index.php

Open http://localhost:3000/. The built-in server is for development: in production, serve index.php with your usual web server.

{
  "name": "email-validation/signup-example",
  "description": "A signup form that validates the email address with the email-validation.org PHP SDK.",
  "type": "project",
  "license": "MIT",
  "require": {
    "php": ">=8.1",
    "email-validation/sdk": "1.1.0"
  },
  "config": {
    "sort-packages": true
  }
}
<?php

// A signup form that checks the email address with the email-validation.org SDK before it creates the account.
// Run: EV_API_KEY=your_key php -S localhost:3000 index.php, then open http://localhost:3000/

declare(strict_types=1);

require __DIR__ . '/vendor/autoload.php';

use EmailValidation\Client;
use EmailValidation\Exception\EmailValidationException;

/** The decision for one address: whether to create the account, and what to tell the person. */
function check(string $email): array
{
    if ($email === '') {
        return [false, 'Please enter your email address.'];
    }
    $baseUrl = getenv('EV_BASE_URL');
    $client = new Client(
        apiKey: (string) getenv('EV_API_KEY'),
        baseUrl: $baseUrl !== false && $baseUrl !== '' ? $baseUrl : null, // e.g. the EU endpoint
        timeout: (int) (getenv('EV_TIMEOUT_MS') ?: 10000) / 1000, // a few seconds above the 6 s the service may take
        maxRetries: 0, // a signup form does not wait for retries: it lets the person in instead
    );
    try {
        $result = $client->validate($email, timeout: 6); // the service answers within 6 s
    } catch (EmailValidationException $e) {
        // no answer, rate limited, out of credits …: never block a real person because of it (fail open)
        error_log('signup accepted without a check: ' . $e->errorCode);
        return [true, 'Thanks for signing up!'];
    }
    error_log("signup check: {$result->status} ({$result->reason})"); // never log the address itself
    if ($result->status === 'invalid') {
        $hint = $result->didYouMean !== null ? " Did you mean {$result->didYouMean}?" : '';
        return [false, "We could not deliver to this address ({$result->reason}). Please check it for typos.{$hint}"];
    }
    if ($result->status === 'disposable' && getenv('BLOCK_DISPOSABLE') !== 'false') {
        return [false, 'Please use an address you will keep, not a disposable one.'];
    }
    // valid, catch_all, unknown (and disposable when allowed): create the account here
    return [true, 'Thanks for signing up!'];
}

function page(string $message = ''): string
{
    $note = $message !== '' ? '<p>' . htmlspecialchars($message) . "</p>\n" : '';
    return <<<HTML
    <!doctype html>
    <html lang="en">
    <head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>Sign up</title></head>
    <body>
    <h1>Sign up</h1>
    {$note}<form method="post" action="/signup">
    <label>Email <input type="email" name="email" required></label>
    <button type="submit">Sign up</button>
    </form>
    </body>
    </html>

    HTML;
}

header('Content-Type: text/html; charset=utf-8');
$path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
if ($_SERVER['REQUEST_METHOD'] === 'GET' && $path === '/') {
    echo page();
} elseif ($_SERVER['REQUEST_METHOD'] === 'POST' && $path === '/signup') {
    [$ok, $message] = check(trim((string) ($_POST['email'] ?? '')));
    http_response_code($ok ? 200 : 422);
    echo page($message);
} else {
    http_response_code(404);
    echo page('Not found.');
}

Ruby

Ruby 3.2 or later, WEBrick and the Ruby SDK from Bundler. Save the two files in a new folder, then:

bundle install
export EV_API_KEY=your_api_key
bundle exec ruby app.rb

Open http://localhost:3000/.

# frozen_string_literal: true

source "https://rubygems.org"

ruby ">= 3.2"

gem "email-validation-sdk", "1.1.0"
gem "webrick", "1.9.2"
# A signup form that checks the email address with the email-validation.org SDK before it creates the account.
# Run: EV_API_KEY=your_key bundle exec ruby app.rb, then open http://localhost:3000/
require "cgi"
require "email_validation_sdk"
require "webrick"

$stdout.sync = true

EV = EmailValidation::Client.new(
  api_key: ENV.fetch("EV_API_KEY", ""),
  base_url: ENV["EV_BASE_URL"].to_s.empty? ? nil : ENV["EV_BASE_URL"], # e.g. the EU endpoint
  timeout: Integer(ENV.fetch("EV_TIMEOUT_MS", "10000")) / 1000.0, # a few seconds above the 6 s the service may take
  max_retries: 0 # a signup form does not wait for retries: it lets the person in instead
)
BLOCK_DISPOSABLE = ENV["BLOCK_DISPOSABLE"] != "false"

# The decision for one address: whether to create the account, and what to tell the person.
def check(email)
  return [false, "Please enter your email address."] if email.empty?

  begin
    result = EV.validate(email, timeout: 6) # the service answers within 6 s
  rescue EmailValidation::Error => e
    # no answer, rate limited, out of credits …: never block a real person because of it (fail open)
    puts "signup accepted without a check: #{e.code}"
    return [true, "Thanks for signing up!"]
  end
  puts "signup check: #{result.status} (#{result.reason})" # never log the address itself
  if result.status == "invalid"
    hint = result.did_you_mean ? " Did you mean #{result.did_you_mean}?" : ""
    return [false, "We could not deliver to this address (#{result.reason}). Please check it for typos.#{hint}"]
  end
  if result.status == "disposable" && BLOCK_DISPOSABLE
    return [false, "Please use an address you will keep, not a disposable one."]
  end

  # valid, catch_all, unknown (and disposable when allowed): create the account here
  [true, "Thanks for signing up!"]
end

def page(message = "")
  note = message.empty? ? "" : "<p>#{CGI.escapeHTML(message)}</p>\n"
  <<~HTML
    <!doctype html>
    <html lang="en">
    <head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>Sign up</title></head>
    <body>
    <h1>Sign up</h1>
    #{note}<form method="post" action="/signup">
    <label>Email <input type="email" name="email" required></label>
    <button type="submit">Sign up</button>
    </form>
    </body>
    </html>
  HTML
end

port = Integer(ENV.fetch("PORT", "3000"))
server = WEBrick::HTTPServer.new(Port: port, AccessLog: [], Logger: WEBrick::Log.new($stderr, WEBrick::Log::WARN))
server.mount_proc "/" do |req, res|
  res.content_type = "text/html; charset=utf-8"
  if req.request_method == "GET" && req.path == "/"
    res.body = page
  elsif req.request_method == "POST" && req.path == "/signup"
    ok, message = check(req.query["email"].to_s.strip)
    res.status = ok ? 200 : 422
    res.body = page(message)
  else
    res.status = 404
    res.body = page("Not found.")
  end
end
trap("INT") { server.shutdown }
trap("TERM") { server.shutdown }
puts "Listening on http://localhost:#{port}/"
server.start

Java

Available once the Java SDK is on Maven Central: it is not published yet, so mvn cannot fetch it today. Java 17 or later and Maven, only the JDK's com.sun.net.httpserver and the SDK. Save pom.xml in a new folder and SignupApp.java under src/main/java/example/, then:

export EV_API_KEY=your_api_key
mvn -q compile exec:java

Open http://localhost:3000/.

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <groupId>example</groupId>
  <artifactId>signup</artifactId>
  <version>1.0.0</version>
  <description>A signup form that validates the email address with the email-validation.org Java SDK.</description>

  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <maven.compiler.release>17</maven.compiler.release>
  </properties>

  <!-- Exact versions only, no ranges: Maven resolves this file the same way every time. -->
  <dependencies>
    <dependency>
      <groupId>org.email-validation</groupId>
      <artifactId>email-validation-sdk</artifactId>
      <version>1.1.0</version>
    </dependency>
  </dependencies>

  <build>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-compiler-plugin</artifactId>
        <version>3.16.0</version>
        <configuration>
          <compilerArgs>
            <arg>-Xlint:all</arg>
            <arg>-Werror</arg>
          </compilerArgs>
        </configuration>
      </plugin>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-resources-plugin</artifactId>
        <version>3.5.0</version>
      </plugin>
      <!-- mvn compile exec:java starts the app -->
      <plugin>
        <groupId>org.codehaus.mojo</groupId>
        <artifactId>exec-maven-plugin</artifactId>
        <version>3.6.4</version>
        <configuration>
          <mainClass>example.SignupApp</mainClass>
        </configuration>
      </plugin>
    </plugins>
  </build>
</project>
// A signup form that checks the email address with the email-validation.org SDK before it creates the account.
// Run: EV_API_KEY=your_key mvn -q compile exec:java, then open http://localhost:3000/
package example;

import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpServer;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.Objects;
import java.util.concurrent.Executors;
import org.email_validation.sdk.EmailValidationClient;
import org.email_validation.sdk.EmailValidationException;
import org.email_validation.sdk.Status;
import org.email_validation.sdk.ValidateOptions;
import org.email_validation.sdk.ValidationResult;

public final class SignupApp {
  private static final EmailValidationClient EV = client();
  private static final boolean BLOCK_DISPOSABLE = !"false".equals(System.getenv("BLOCK_DISPOSABLE"));

  private static EmailValidationClient client() {
    EmailValidationClient.Builder builder =
        EmailValidationClient.builder(Objects.requireNonNullElse(System.getenv("EV_API_KEY"), ""))
            // a few seconds above the 6 s the service may take
            .timeout(Duration.ofMillis(Long.parseLong(env("EV_TIMEOUT_MS", "10000"))))
            .maxRetries(0); // a signup form does not wait for retries: it lets the person in instead
    String baseUrl = System.getenv("EV_BASE_URL");
    if (baseUrl != null && !baseUrl.isEmpty()) builder.baseUrl(baseUrl); // e.g. the EU endpoint
    return builder.build();
  }

  /** The decision for one address: whether to create the account, and what to tell the person. */
  record Decision(boolean ok, String message) {}

  static Decision check(String email) {
    if (email.isEmpty()) return new Decision(false, "Please enter your email address.");
    ValidationResult result;
    try {
      // the service answers within 6 s
      result = EV.validate(email, ValidateOptions.builder().timeoutSeconds(6).build());
    } catch (EmailValidationException e) {
      // no answer, rate limited, out of credits …: never block a real person because of it (fail open)
      System.err.println("signup accepted without a check: " + e.code());
      return new Decision(true, "Thanks for signing up!");
    }
    System.out.println("signup check: " + result.status() + " (" + result.reason() + ")"); // never the address
    if (Status.INVALID.equals(result.status())) {
      String hint = result.didYouMean() != null ? " Did you mean " + result.didYouMean() + "?" : "";
      return new Decision(
          false,
          "We could not deliver to this address (" + result.reason() + "). Please check it for typos." + hint);
    }
    if (Status.DISPOSABLE.equals(result.status()) && BLOCK_DISPOSABLE) {
      return new Decision(false, "Please use an address you will keep, not a disposable one.");
    }
    // valid, catch_all, unknown (and disposable when allowed): create the account here
    return new Decision(true, "Thanks for signing up!");
  }

  static String escapeHtml(String s) {
    return s.replace("&", "&amp;")
        .replace("<", "&lt;")
        .replace(">", "&gt;")
        .replace("\"", "&quot;")
        .replace("'", "&#39;");
  }

  static String page(String message) {
    return """
        <!doctype html>
        <html lang="en">
        <head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>Sign up</title></head>
        <body>
        <h1>Sign up</h1>
        %s<form method="post" action="/signup">
        <label>Email <input type="email" name="email" required></label>
        <button type="submit">Sign up</button>
        </form>
        </body>
        </html>
        """
        .formatted(message.isEmpty() ? "" : "<p>" + escapeHtml(message) + "</p>\n");
  }

  static void send(HttpExchange exchange, int status, String html) throws IOException {
    byte[] body = html.getBytes(StandardCharsets.UTF_8);
    exchange.getResponseHeaders().set("Content-Type", "text/html; charset=utf-8");
    exchange.sendResponseHeaders(status, body.length);
    try (var out = exchange.getResponseBody()) {
      out.write(body);
    }
  }

  /** The email field of a form post (application/x-www-form-urlencoded). */
  static String emailField(String body) {
    for (String pair : body.split("&")) {
      int eq = pair.indexOf('=');
      String name = URLDecoder.decode(eq < 0 ? pair : pair.substring(0, eq), StandardCharsets.UTF_8);
      if (name.equals("email")) {
        return eq < 0 ? "" : URLDecoder.decode(pair.substring(eq + 1), StandardCharsets.UTF_8).trim();
      }
    }
    return "";
  }

  static void handle(HttpExchange exchange) throws IOException {
    try (exchange) {
      String method = exchange.getRequestMethod();
      String path = exchange.getRequestURI().getPath();
      if (method.equals("GET") && path.equals("/")) {
        send(exchange, 200, page(""));
      } else if (method.equals("POST") && path.equals("/signup")) {
        byte[] body = exchange.getRequestBody().readNBytes(10_001);
        if (body.length > 10_000) {
          send(exchange, 413, page("That is too long."));
          return;
        }
        Decision d;
        try {
          d = check(emailField(new String(body, StandardCharsets.UTF_8)));
        } catch (RuntimeException e) {
          System.err.println("signup failed: " + e.getClass().getSimpleName());
          send(exchange, 500, page("Something went wrong. Please try again."));
          return;
        }
        send(exchange, d.ok() ? 200 : 422, page(d.message()));
      } else {
        send(exchange, 404, page("Not found."));
      }
    }
  }

  private static String env(String name, String fallback) {
    String value = System.getenv(name);
    return value == null || value.isEmpty() ? fallback : value;
  }

  public static void main(String[] args) throws IOException {
    int port = Integer.parseInt(env("PORT", "3000"));
    HttpServer server = HttpServer.create(new InetSocketAddress(port), 0);
    server.createContext("/", SignupApp::handle);
    server.setExecutor(Executors.newCachedThreadPool());
    server.start();
    System.out.println("Listening on http://localhost:" + port + "/");
  }
}

C#

.NET 10, an ASP.NET Core minimal API and the C# SDK from NuGet. Save the two files in a new folder, then:

dotnet restore
export EV_API_KEY=your_api_key
dotnet run

Open http://localhost:3000/.

<Project Sdk="Microsoft.NET.Sdk.Web">

  <PropertyGroup>
    <TargetFramework>net10.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
    <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
    <!-- packages.lock.json pins every package; a locked-mode restore installs exactly those -->
    <RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="EmailValidation.Sdk" Version="1.1.0" />
  </ItemGroup>

</Project>
// A signup form that checks the email address with the email-validation.org SDK before it creates the account.
// Run: EV_API_KEY=your_key dotnet run, then open http://localhost:3000/
using System.Net;
using EmailValidation;

var options = new EmailValidationClientOptions
{
    // a few seconds above the 6 s the service may take
    Timeout = TimeSpan.FromMilliseconds(int.Parse(Environment.GetEnvironmentVariable("EV_TIMEOUT_MS") ?? "10000")),
    MaxRetries = 0, // a signup form does not wait for retries: it lets the person in instead
};
var baseUrl = Environment.GetEnvironmentVariable("EV_BASE_URL");
if (!string.IsNullOrEmpty(baseUrl)) options.BaseUrl = baseUrl; // e.g. the EU endpoint
using var ev = new EmailValidationClient(Environment.GetEnvironmentVariable("EV_API_KEY") ?? "", options);
var blockDisposable = Environment.GetEnvironmentVariable("BLOCK_DISPOSABLE") != "false";

var app = WebApplication.CreateBuilder(args).Build();

app.MapGet("/", () => Page(200, ""));

app.MapPost("/signup", async (HttpRequest request) =>
{
    var form = await request.ReadFormAsync();
    var email = form["email"].ToString().Trim();
    var (ok, message) = await Check(email);
    return Page(ok ? 200 : 422, message);
});

var port = Environment.GetEnvironmentVariable("PORT") ?? "3000";
app.Run($"http://0.0.0.0:{port}");

// The decision for one address: whether to create the account, and what to tell the person.
async Task<(bool Ok, string Message)> Check(string email)
{
    if (email.Length == 0) return (false, "Please enter your email address.");
    ValidationResult result;
    try
    {
        result = await ev.ValidateAsync(email, new ValidateOptions { TimeoutSeconds = 6 }); // the service answers within 6 s
    }
    catch (EmailValidationException e)
    {
        // no answer, rate limited, out of credits …: never block a real person because of it (fail open)
        Console.Error.WriteLine($"signup accepted without a check: {e.Code}");
        return (true, "Thanks for signing up!");
    }
    Console.WriteLine($"signup check: {result.Status} ({result.Reason})"); // never log the address itself
    if (result.Status == Status.Invalid)
    {
        var hint = result.DidYouMean is { } suggestion ? $" Did you mean {suggestion}?" : "";
        return (false, $"We could not deliver to this address ({result.Reason}). Please check it for typos.{hint}");
    }
    if (result.Status == Status.Disposable && blockDisposable)
        return (false, "Please use an address you will keep, not a disposable one.");
    // valid, catch_all, unknown (and disposable when allowed): create the account here
    return (true, "Thanks for signing up!");
}

static IResult Page(int status, string message) => Results.Content($"""
    <!doctype html>
    <html lang="en">
    <head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>Sign up</title></head>
    <body>
    <h1>Sign up</h1>
    {(message.Length > 0 ? $"<p>{WebUtility.HtmlEncode(message)}</p>\n" : "")}<form method="post" action="/signup">
    <label>Email <input type="email" name="email" required></label>
    <button type="submit">Sign up</button>
    </form>
    </body>
    </html>

    """, "text/html; charset=utf-8", statusCode: status);

Rust

Rust 1.88 or later, axum on Tokio and the Rust SDK. Save Cargo.toml in a new folder and main.rs under src/, then:

cargo build
export EV_API_KEY=your_api_key
cargo run

Open http://localhost:3000/.

[package]
name = "email-validation-signup-example"
version = "1.0.0"
edition = "2024"
rust-version = "1.88"
license = "MIT"
description = "A signup form that validates the email address with the email-validation.org Rust SDK."
publish = false

[dependencies]
email-validation-sdk = "1.1"
axum = { version = "0.8", default-features = false, features = ["form", "http1", "tokio"] }
serde = { version = "1", features = ["derive"] }
tokio = { version = "1", features = ["macros", "rt-multi-thread", "net"] }
// A signup form that checks the email address with the email-validation.org SDK before it creates the account.
// Run: EV_API_KEY=your_key cargo run, then open http://localhost:3000/
use std::{env, time::Duration};

use axum::{
    Form, Router,
    extract::State,
    http::StatusCode,
    response::Html,
    routing::{get, post},
};
use email_validation::{Client, Status, ValidateOptions};
use serde::Deserialize;

#[derive(Clone)]
struct App {
    ev: Client,
    block_disposable: bool,
}

#[derive(Deserialize)]
struct Signup {
    #[serde(default)]
    email: String,
}

/// The decision for one address: whether to create the account, and what to tell the person.
async fn check(app: &App, email: &str) -> (bool, String) {
    if email.is_empty() {
        return (false, "Please enter your email address.".into());
    }
    // the service answers within 6 s
    let result = match app
        .ev
        .validate_with(email, ValidateOptions::new().timeout(6))
        .await
    {
        Ok(result) => result,
        Err(err) => {
            // no answer, rate limited, out of credits …: never block a real person because of it (fail open)
            eprintln!("signup accepted without a check: {}", err.code());
            return (true, "Thanks for signing up!".into());
        }
    };
    println!("signup check: {} ({})", result.status, result.reason); // never log the address itself
    match result.status {
        Status::Invalid => {
            let hint = match &result.did_you_mean {
                Some(suggestion) => format!(" Did you mean {suggestion}?"),
                None => String::new(),
            };
            let reason = &result.reason;
            (
                false,
                format!(
                    "We could not deliver to this address ({reason}). Please check it for typos.{hint}"
                ),
            )
        }
        Status::Disposable if app.block_disposable => (
            false,
            "Please use an address you will keep, not a disposable one.".into(),
        ),
        // valid, catch_all, unknown (and disposable when allowed): create the account here
        _ => (true, "Thanks for signing up!".into()),
    }
}

fn escape_html(s: &str) -> String {
    s.replace('&', "&amp;")
        .replace('<', "&lt;")
        .replace('>', "&gt;")
        .replace('"', "&quot;")
        .replace('\'', "&#39;")
}

fn page(message: &str) -> Html<String> {
    let message = if message.is_empty() {
        String::new()
    } else {
        format!("<p>{}</p>\n", escape_html(message))
    };
    Html(format!(
        r#"<!doctype html>
<html lang="en">
<head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>Sign up</title></head>
<body>
<h1>Sign up</h1>
{message}<form method="post" action="/signup">
<label>Email <input type="email" name="email" required></label>
<button type="submit">Sign up</button>
</form>
</body>
</html>
"#
    ))
}

async fn signup(State(app): State<App>, Form(form): Form<Signup>) -> (StatusCode, Html<String>) {
    let (ok, message) = check(&app, form.email.trim()).await;
    let status = if ok {
        StatusCode::OK
    } else {
        StatusCode::UNPROCESSABLE_ENTITY
    };
    (status, page(&message))
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let mut builder = Client::builder(env::var("EV_API_KEY").unwrap_or_default())
        // a few seconds above the 6 s the service may take
        .timeout(Duration::from_millis(
            env::var("EV_TIMEOUT_MS")
                .ok()
                .and_then(|v| v.parse().ok())
                .unwrap_or(10_000),
        ))
        .max_retries(0); // a signup form does not wait for retries: it lets the person in instead
    if let Ok(base_url) = env::var("EV_BASE_URL") {
        builder = builder.base_url(base_url); // e.g. the EU endpoint
    }
    let app = App {
        ev: builder.build()?,
        block_disposable: env::var("BLOCK_DISPOSABLE").map_or(true, |v| v != "false"),
    };
    let router = Router::new()
        .route("/", get(|| async { page("") }))
        .route("/signup", post(signup))
        .with_state(app);
    let port: u16 = env::var("PORT")
        .ok()
        .and_then(|v| v.parse().ok())
        .unwrap_or(3000);
    let listener = tokio::net::TcpListener::bind(("0.0.0.0", port)).await?;
    println!("Listening on http://localhost:{port}/");
    axum::serve(listener, router).await?;
    Ok(())
}

Swift

Swift 6 on macOS 14 or later, Hummingbird 2 and the Swift SDK. Swift has no HTTP server in its standard library or in Foundation, so a web form needs a server package; Hummingbird is a small one on SwiftNIO. Save Package.swift in a new folder and main.swift under Sources/Signup/, then:

swift build
export EV_API_KEY=your_api_key
swift run Signup

Open http://localhost:3000/.

// swift-tools-version:6.0
// A signup form that validates the email address with the email-validation.org Swift SDK.
import PackageDescription

let package = Package(
    name: "Signup",
    platforms: [.macOS(.v14)],
    dependencies: [
        .package(url: "https://github.com/email-validation-dev/email-validation-swift.git", from: "1.1.0"),
        .package(url: "https://github.com/hummingbird-project/hummingbird.git", from: "2.28.0"),
    ],
    targets: [
        .executableTarget(
            name: "Signup",
            dependencies: [
                .product(name: "EmailValidation", package: "email-validation-swift"),
                .product(name: "Hummingbird", package: "hummingbird"),
            ]
        )
    ]
)
// A signup form that checks the email address with the email-validation.org SDK before it creates the account.
// Run: EV_API_KEY=your_key swift run, then open http://localhost:3000/
import EmailValidation
import Foundation
import Hummingbird

let env = ProcessInfo.processInfo.environment

/// The decision for one address: whether to create the account, and what to tell the person.
struct Signup: Sendable {
    let ev: EmailValidationClient
    let blockDisposable: Bool

    func check(_ email: String) async -> (ok: Bool, message: String) {
        if email.isEmpty { return (false, "Please enter your email address.") }
        let result: ValidationResult
        do {
            result = try await ev.validate(email, timeout: 6)  // the service answers within 6 s
        } catch {
            // no answer, rate limited, out of credits …: never block a real person because of it (fail open)
            print("signup accepted without a check: \((error as? EmailValidationError)?.code.rawValue ?? "error")")
            return (true, "Thanks for signing up!")
        }
        print("signup check: \(result.status.rawValue) (\(result.reason.rawValue))")  // never log the address itself
        if result.status == .invalid {
            let hint = result.didYouMean.map { " Did you mean \($0)?" } ?? ""
            return (
                false,
                "We could not deliver to this address (\(result.reason.rawValue)). Please check it for typos.\(hint)"
            )
        }
        if result.status == .disposable && blockDisposable {
            return (false, "Please use an address you will keep, not a disposable one.")
        }
        // valid, catch_all, unknown (and disposable when allowed): create the account here
        return (true, "Thanks for signing up!")
    }
}

func escapeHTML(_ s: String) -> String {
    s.replacingOccurrences(of: "&", with: "&amp;").replacingOccurrences(of: "<", with: "&lt;")
        .replacingOccurrences(of: ">", with: "&gt;").replacingOccurrences(of: "\"", with: "&quot;")
        .replacingOccurrences(of: "'", with: "&#39;")
}

func page(_ status: HTTPResponse.Status, _ message: String = "") -> Response {
    let html = """
        <!doctype html>
        <html lang="en">
        <head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>Sign up</title></head>
        <body>
        <h1>Sign up</h1>
        \(message.isEmpty ? "" : "<p>\(escapeHTML(message))</p>\n")<form method="post" action="/signup">
        <label>Email <input type="email" name="email" required></label>
        <button type="submit">Sign up</button>
        </form>
        </body>
        </html>

        """
    return Response(
        status: status,
        headers: [.contentType: "text/html; charset=utf-8"],
        body: .init(byteBuffer: ByteBuffer(string: html))
    )
}

struct SignupForm: Decodable {
    var email: String?
}

let signup = Signup(
    ev: try EmailValidationClient(
        apiKey: env["EV_API_KEY"] ?? "",
        // EV_BASE_URL: e.g. the EU endpoint
        baseURL: env["EV_BASE_URL"].flatMap { $0.isEmpty ? nil : $0 } ?? Vocabulary.defaultBaseURL,
        // a few seconds above the 6 s the service may take
        timeout: (Double(env["EV_TIMEOUT_MS"] ?? "") ?? 10_000) / 1000,
        maxRetries: 0  // a signup form does not wait for retries: it lets the person in instead
    ),
    blockDisposable: env["BLOCK_DISPOSABLE"] != "false"
)

let router = Router()
router.get("/") { _, _ in page(.ok) }
router.post("/signup") { request, _ -> Response in
    let body = try await request.body.collect(upTo: 10_000)
    let form = (try? URLEncodedFormDecoder().decode(SignupForm.self, from: String(buffer: body))) ?? SignupForm()
    let email = (form.email ?? "").trimmingCharacters(in: .whitespacesAndNewlines)
    let (ok, message) = await signup.check(email)
    return page(ok ? .ok : .unprocessableContent, message)
}

let port = Int(env["PORT"] ?? "") ?? 3000
let app = Application(router: router, configuration: .init(address: .hostname("0.0.0.0", port: port)))
print("Listening on http://localhost:\(port)/")
try await app.runService()

Kotlin

Available once the Kotlin SDK is on Maven Central: it is not published yet, so Gradle cannot fetch it today. JDK 17 or later and Gradle 9, only the JDK's com.sun.net.httpserver and the SDK (its calls are suspend functions, so each request runs one in runBlocking). Save build.gradle.kts and an empty settings.gradle.kts in a new folder and Main.kt under src/main/kotlin/, then:

gradle classes
export EV_API_KEY=your_api_key
gradle run

Open http://localhost:3000/.

plugins {
    kotlin("jvm") version "2.4.10"
    application
}

repositories {
    mavenCentral()
}

dependencies {
    implementation("org.email-validation:email-validation-sdk-kotlin:1.1.0")
}

java {
    sourceCompatibility = JavaVersion.VERSION_17
    targetCompatibility = JavaVersion.VERSION_17
}

kotlin {
    compilerOptions {
        jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17)
        allWarningsAsErrors.set(true)
    }
}

application {
    mainClass.set("MainKt")
}

// gradle.lockfile pins every dependency: a build never picks a version this file does not name
dependencyLocking {
    lockAllConfigurations()
}
// A signup form that checks the email address with the email-validation.org SDK before it creates the account.
// Run: EV_API_KEY=your_key gradle run, then open http://localhost:3000/
import com.sun.net.httpserver.HttpExchange
import com.sun.net.httpserver.HttpServer
import java.net.InetSocketAddress
import java.net.URLDecoder
import java.util.concurrent.Executors
import kotlin.time.Duration.Companion.milliseconds
import kotlinx.coroutines.runBlocking
import org.email_validation.kotlin.EmailValidationClient
import org.email_validation.kotlin.EmailValidationException
import org.email_validation.kotlin.Status

val ev = EmailValidationClient(System.getenv("EV_API_KEY") ?: "") {
    System.getenv("EV_BASE_URL")?.takeIf { it.isNotEmpty() }?.let { baseUrl = it } // e.g. the EU endpoint
    // a few seconds above the 6 s the service may take
    timeout = (System.getenv("EV_TIMEOUT_MS")?.toLongOrNull() ?: 10_000).milliseconds
    maxRetries = 0 // a signup form does not wait for retries: it lets the person in instead
}
val blockDisposable = System.getenv("BLOCK_DISPOSABLE") != "false"

/** The decision for one address: whether to create the account, and what to tell the person. */
suspend fun check(email: String): Pair<Boolean, String> {
    if (email.isEmpty()) return false to "Please enter your email address."
    val result = try {
        ev.validate(email, timeout = 6) // the service answers within 6 s
    } catch (e: EmailValidationException) {
        // no answer, rate limited, out of credits …: never block a real person because of it (fail open)
        System.err.println("signup accepted without a check: ${e.code}")
        return true to "Thanks for signing up!"
    }
    println("signup check: ${result.status} (${result.reason})") // never log the address itself
    if (result.status == Status.INVALID) {
        val hint = result.didYouMean?.let { " Did you mean $it?" } ?: ""
        return false to "We could not deliver to this address (${result.reason}). Please check it for typos.$hint"
    }
    if (result.status == Status.DISPOSABLE && blockDisposable) {
        return false to "Please use an address you will keep, not a disposable one."
    }
    // valid, catch_all, unknown (and disposable when allowed): create the account here
    return true to "Thanks for signing up!"
}

fun escapeHtml(s: String): String =
    s.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;").replace("\"", "&quot;").replace("'", "&#39;")

fun page(message: String = ""): String = """
    |<!doctype html>
    |<html lang="en">
    |<head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>Sign up</title></head>
    |<body>
    |<h1>Sign up</h1>
    |${if (message.isEmpty()) "" else "<p>${escapeHtml(message)}</p>\n"}<form method="post" action="/signup">
    |<label>Email <input type="email" name="email" required></label>
    |<button type="submit">Sign up</button>
    |</form>
    |</body>
    |</html>
    |""".trimMargin()

fun send(exchange: HttpExchange, status: Int, html: String) {
    val body = html.toByteArray()
    exchange.responseHeaders.set("Content-Type", "text/html; charset=utf-8")
    exchange.sendResponseHeaders(status, body.size.toLong())
    exchange.responseBody.use { it.write(body) }
}

/** The email field of a form post (application/x-www-form-urlencoded). */
fun emailField(body: String): String = body.split("&").firstNotNullOfOrNull { pair ->
    val (name, value) = pair.split("=", limit = 2).map { URLDecoder.decode(it, Charsets.UTF_8) } + ""
    if (name == "email") value.trim() else null
} ?: ""

fun handle(exchange: HttpExchange) = exchange.use {
    val path = exchange.requestURI.path
    when {
        exchange.requestMethod == "GET" && path == "/" -> send(exchange, 200, page())
        exchange.requestMethod == "POST" && path == "/signup" -> {
            val body = exchange.requestBody.readNBytes(10_001)
            if (body.size > 10_000) return@use send(exchange, 413, page("That is too long."))
            val (ok, message) = try {
                runBlocking { check(emailField(body.decodeToString())) }
            } catch (e: RuntimeException) {
                System.err.println("signup failed: ${e.javaClass.simpleName}")
                return@use send(exchange, 500, page("Something went wrong. Please try again."))
            }
            send(exchange, if (ok) 200 else 422, page(message))
        }
        else -> send(exchange, 404, page("Not found."))
    }
}

fun main() {
    val port = System.getenv("PORT")?.toIntOrNull() ?: 3000
    val server = HttpServer.create(InetSocketAddress(port), 0)
    server.createContext("/") { handle(it) }
    server.executor = Executors.newCachedThreadPool()
    server.start()
    println("Listening on http://localhost:$port/")
}

Updated · Maintained by the email-validation.org team