Why you need a proxy
React code runs in the browser, and anything in the browser is public. A live key in a React app can be copied and used to spend your credits. Instead, the form calls a small endpoint on your own server, and that endpoint calls VeriDock.
The proxy also lets you rate limit per visitor and choose which fields to return. The form only needs the status, reason and suggestion.
app.get("/api/check-email", async (req, res) => {
const email = String(req.query.email ?? "").slice(0, 254);
if (!email) return res.status(400).json({ error: "email is required" });
// smtp=false: syntax, DNS, MX, disposable and typo checks only. Fast enough for as-you-type.
const qs = new URLSearchParams({ email, smtp: "false" });
const r = await fetch(`https://app.veridock.io/v1/verify?${qs}`, {
headers: { Authorization: `Bearer ${process.env.VERIDOCK_API_KEY}` },
signal: AbortSignal.timeout(5000),
}).catch(() => null);
if (!r?.ok) return res.json({ status: "unknown" }); // fail open
const { status, reason, suggestion } = await r.json();
res.json({ status, reason, suggestion });
});Add rate limiting to this endpoint, per IP or per session, so it cannot be used as a free verification service by someone else.
A debounced hook
Checking on every keystroke would waste requests on half-typed addresses. The hook waits until the user stops typing for 600 ms and the value looks like an address, then sends one request. If the user keeps typing, the AbortController cancels the stale request, so an old answer can never overwrite a newer one.
import { useEffect, useState } from "react";
export type EmailCheck = {
status: "deliverable" | "undeliverable" | "risky" | "unknown";
reason?: string;
suggestion?: string | null;
};
const looksLikeEmail = (v: string) => /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/.test(v);
export function useEmailCheck(email: string, delay = 600) {
const [result, setResult] = useState<EmailCheck | null>(null);
const [checking, setChecking] = useState(false);
useEffect(() => {
setResult(null);
if (!looksLikeEmail(email)) return;
const controller = new AbortController();
const timer = setTimeout(async () => {
setChecking(true);
try {
const res = await fetch(`/api/check-email?email=${encodeURIComponent(email)}`, { signal: controller.signal });
setResult(await res.json());
} catch {
/* aborted or offline: show nothing rather than a false error */
} finally {
if (!controller.signal.aborted) setChecking(false);
}
}, delay);
return () => {
clearTimeout(timer);
controller.abort();
};
}, [email, delay]);
return { result, checking };
}Use it in a form
Show a hint, never a hard block, while the user is typing. The suggestion turns a likely typo into a one-click fix, which is where most of the value is: a signup with gmial.com is a real person you would otherwise lose.
import { useState } from "react";
import { useEmailCheck } from "./useEmailCheck";
const messages: Record<string, string> = {
undeliverable: "This address can't receive email.",
disposable: "Please use a permanent email address.",
};
export function SignupForm() {
const [email, setEmail] = useState("");
const { result, checking } = useEmailCheck(email);
const message =
result?.reason === "disposable" ? messages.disposable : result?.status === "undeliverable" ? messages.undeliverable : null;
return (
<form method="post" action="/signup">
<label htmlFor="email">Email</label>
<input
id="email"
name="email"
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
aria-invalid={Boolean(message)}
aria-describedby="email-hint"
required
/>
<p id="email-hint" aria-live="polite">
{checking && "Checking…"}
{result?.suggestion && (
<>
Did you mean{" "}
<button type="button" onClick={() => setEmail(result.suggestion!)}>
{result.suggestion}
</button>
?
</>
)}
{!result?.suggestion && message}
</p>
<button type="submit">Sign up</button>
</form>
);
}Run the full check on submit
The as-you-type check skips SMTP to stay fast, and anything in the browser can be bypassed. Run a full verification on your server when the form is submitted, with the same accept and reject rules, before creating the account. The Next.js guide shows that step with a Server Action.
Reference
Frequently asked questions
Keep reading
Other guides: Next.js · Node.js · Python · PHP · Supabase · Zapier, Make and n8n