Set up your API key
Create a key in the dashboard and add it to .env.local. Use a vd_test_ key while you build: test keys return fixed results and never use credits. Do not prefix the variable with NEXT_PUBLIC_, or Next.js will bundle it into client JavaScript.
VERIDOCK_API_KEY=vd_test_••••••••••••Write a server-only client
Put the API call in one module and mark it server-only, so importing it from a client component fails the build instead of leaking the key. Passing timeout=4000 tells VeriDock to give up on a slow mail server after four seconds and return unknown, which keeps signup fast. The client-side abort is a little longer, as a backstop.
import "server-only";
export type Verification = {
email: string;
status: "deliverable" | "undeliverable" | "risky" | "unknown";
reason: string;
score: number;
suggestion: string | null;
};
export async function verifyEmail(email: string): Promise<Verification | null> {
const url = new URL("https://app.veridock.io/v1/verify");
url.searchParams.set("email", email);
url.searchParams.set("timeout", "4000");
try {
const res = await fetch(url, {
headers: { Authorization: `Bearer ${process.env.VERIDOCK_API_KEY}` },
signal: AbortSignal.timeout(6000),
cache: "no-store",
});
if (!res.ok) {
const { error } = await res.json().catch(() => ({ error: null }));
console.error("VeriDock error", res.status, error?.code);
return null;
}
return (await res.json()) as Verification;
} catch (err) {
console.error("VeriDock unreachable", err);
return null;
}
}Decide what to accept
Keep the policy in one place. A sensible default for a SaaS signup is to block addresses that will bounce and throwaway inboxes, and let everything else through. If the check itself fails, or the result is unknown, fail open: losing a real user because a mail server was slow is worse than letting one unconfirmed address in.
- undeliverable: reject, and show the suggestion if there is one.
- risky with reason disposable: reject.
- risky for other reasons (role_account, catch_all): accept, optionally flag for review.
- unknown, or the API call failed: accept.
Call it from a Server Action
The action returns a small state object that the form renders. It only creates the account once the address passes.
"use server";
import { redirect } from "next/navigation";
import { verifyEmail } from "@/lib/veridock";
export type SignupState = { error?: string; suggestion?: string };
export async function signup(_prev: SignupState, formData: FormData): Promise<SignupState> {
const email = String(formData.get("email") ?? "").trim();
const result = await verifyEmail(email);
if (result?.status === "undeliverable") {
return result.suggestion
? { error: "That address looks mistyped.", suggestion: result.suggestion }
: { error: "That address can't receive email. Please check it." };
}
if (result?.status === "risky" && result.reason === "disposable") {
return { error: "Please use a permanent email address." };
}
await createUser(email); // your existing signup logic
redirect("/welcome");
}Render errors and suggestions in the form
useActionState wires the action to the form and gives you its latest return value. The suggestion button fills the corrected address in, so the user fixes a typo with one click.
"use client";
import { useActionState, useRef } from "react";
import { signup, type SignupState } from "./actions";
export function SignupForm() {
const [state, action, pending] = useActionState<SignupState, FormData>(signup, {});
const input = useRef<HTMLInputElement>(null);
return (
<form action={action}>
<label htmlFor="email">Work email</label>
<input ref={input} id="email" name="email" type="email" required aria-describedby="email-error" />
{state.error && (
<p id="email-error" role="alert">
{state.error}{" "}
{state.suggestion && (
<button type="button" onClick={() => input.current && (input.current.value = state.suggestion!)}>
Use {state.suggestion}
</button>
)}
</p>
)}
<button type="submit" disabled={pending}>
{pending ? "Checking…" : "Create account"}
</button>
</form>
);
}Test it
With a vd_test_ key, run through each branch: a valid address, a mistyped domain, a disposable domain and an address on a domain that does not exist. Then swap in a vd_live_ key in your production environment variables. The free plan's 250 monthly checks cover most early-stage signup volume.
Reference
Frequently asked questions
Keep reading
Other guides: Node.js · Python · React · PHP · Supabase · Zapier, Make and n8n