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:
| Result | Answer |
|---|---|
valid, catch_all, unknown | Accepted: "Thanks for signing up!" |
invalid | Refused, with the reason, and the suggestion when did_you_mean is set |
disposable | Refused ("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 error | Accepted: 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 startOpen 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) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' })[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.pyOpen 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.phpOpen 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.rbOpen 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.startJava
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:javaOpen 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("&", "&")
.replace("<", "<")
.replace(">", ">")
.replace("\"", """)
.replace("'", "'");
}
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 runOpen 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 runOpen 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('&', "&")
.replace('<', "<")
.replace('>', ">")
.replace('"', """)
.replace('\'', "'")
}
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 SignupOpen 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: "&").replacingOccurrences(of: "<", with: "<")
.replacingOccurrences(of: ">", with: ">").replacingOccurrences(of: "\"", with: """)
.replacingOccurrences(of: "'", with: "'")
}
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 runOpen 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("&", "&").replace("<", "<").replace(">", ">").replace("\"", """).replace("'", "'")
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/")
}