Verify email addresses in Next.js
In Next.js, call the SimpleVerifier API from server code only: a server action or a route handler. Read the key from process.env.SIMPLEVERIFIER_KEY without a NEXT_PUBLIC_ prefix so it is never sent to the browser, reject only status invalid, and treat timeouts as unknown so a slow mail server never blocks a signup.
By SimpleVerifier Team
Before you start
- Create a key on the API keys page. Keys start with
sv_live_and are shown once. - Add
SIMPLEVERIFIER_KEYto.env.localand to your host's environment settings. Never prefix it withNEXT_PUBLIC_; that would put it in the client bundle. - API access is included in the Pro plan ($29.99/month) and the Email + Phone plan ($49.99/month). See pricing.
1. Add a server-side helper
Typed, no dependencies, never throws. Import it only from server actions, route handlers and server components.
// lib/simpleverifier.ts — server code only; never import this from a client component
export type EmailStatus = "valid" | "risky" | "invalid";
const API_URL = "https://simpleverifier.com/api/v1/verify";
const EMAIL_SHAPE = /^[^@\s]+@[^@\s]+\.[^@\s]+$/;
/** Resolves to the verdict, or null when the API could not answer. Never throws. */
export async function emailStatus(email: string): Promise<EmailStatus | null> {
const address = email.trim();
if (!EMAIL_SHAPE.test(address)) return "invalid";
try {
const res = await fetch(`${API_URL}?email=${encodeURIComponent(address)}`, {
headers: { Authorization: `Bearer ${process.env.SIMPLEVERIFIER_KEY}` },
signal: AbortSignal.timeout(30_000),
cache: "no-store",
});
if (!res.ok) return null; // 401, 402, 429, 503: fail open
const data = (await res.json()) as { status?: EmailStatus };
return data.status ?? null;
} catch {
return null; // timeout or network error: fail open
}
}2. Check the address in a server action
The action runs on the server, so the key stays there.
// app/signup/actions.ts
"use server";
import { emailStatus } from "@/lib/simpleverifier";
export interface SignupState {
ok?: boolean;
error?: string;
}
export async function signup(_prev: SignupState, formData: FormData): Promise<SignupState> {
const email = String(formData.get("email") ?? "");
const status = await emailStatus(email);
if (status === "invalid") {
return { error: "This email address cannot receive mail. Please check it." };
}
// Save the signup here. status is "valid", "risky" or null (API unavailable).
return { ok: true };
}3. Wire it to a form
useActionState (React 19) gives you the returned error and a pending flag for the button.
// app/signup/SignupForm.tsx
"use client";
import { useActionState } from "react";
import { signup, type SignupState } from "./actions";
export default function SignupForm() {
const [state, formAction, pending] = useActionState<SignupState, FormData>(signup, {});
if (state.ok) return <p>Thanks, you're signed up.</p>;
return (
<form action={formAction}>
<label htmlFor="email">Email</label>
<input id="email" name="email" type="email" required aria-describedby="email-error" />
<button type="submit" disabled={pending}>
{pending ? "Checking…" : "Sign up"}
</button>
<p id="email-error" role="alert">
{state.error}
</p>
</form>
);
}4. Or expose a route handler
Useful when a client component wants to check the address as the visitor types (see the React form guide). Add rate limiting to it, since anyone can call a public route.
// app/api/check-email/route.ts
import { NextResponse } from "next/server";
import { emailStatus } from "@/lib/simpleverifier";
export async function POST(request: Request) {
const body = (await request.json().catch(() => ({}))) as { email?: unknown };
const email = typeof body.email === "string" ? body.email : "";
const status = await emailStatus(email);
return NextResponse.json({ status });
}What the API returns
GET or POST https://simpleverifier.com/api/v1/verify with email runs the syntax, domain, disposable-domain and live mailbox checks in one call and returns:
{
"email": "jane@company.com",
"status": "valid",
"reason": "Mailbox exists",
"mailbox_checked": true
}Every snippet above follows the same rule: only a successful response with status set to invalid blocks a person.
| Result | What it means | What to do |
|---|---|---|
| valid | The mail server confirmed the mailbox exists. | Accept. |
| risky | The domain is real but the mailbox couldn't be confirmed: catch-all domains, role addresses like info@, providers that hide mailbox status. | Accept on signup forms. Store the status and keep these out of cold campaigns. |
| invalid | Bad format, no mail server, a disposable domain, or the mailbox doesn't exist. | Reject and ask the person to check the address. |
| timeout, 429 or 503 | The check didn't finish: a slow mail server, too many calls this minute, or a brief outage. | Accept (fail open) and log it. Re-check later if it matters. |
| 401 or 402 | The key is wrong or revoked, or the subscription isn't active. | Accept, and alert yourself: every check is being skipped until it's fixed. |
Errors return JSON with an error message. Each key can make 120 calls a minute; for thousands of addresses, upload a CSV in the dashboard instead.
Phone numbers and spreadsheets
The same key works with /api/v1/phone?phone=…&country=… to validate a phone number and normalise it to E.164, with live carrier lookups on the Email + Phone plan. The request and response are in the API reference. To check a column of addresses without code, use the Google Sheets add-on with the same key.
Common questions
- Will Next.js cache the API response?
- The helper passes cache: "no-store" so every call reaches the API. If you want to avoid re-checking the same address, cache the status in your own database keyed by the address.
- Does this run on the Edge runtime?
- Yes. The helper only uses fetch and AbortSignal.timeout, which both exist on the Edge runtime as well as Node.js.
- How much does the SimpleVerifier API cost?
- API access is included in the paid plans: Pro at $29.99/month and Email + Phone at $49.99/month. There are no per-call credits. Each key can make 120 calls a minute.
- Should I reject risky addresses?
- Not on a signup or contact form. Risky means the mailbox couldn't be confirmed, which includes every address at a catch-all company domain. Accept it, store the status, and leave risky addresses out of cold outreach.
- What if the API is slow or down?
- Let the address through. A mailbox check can take several seconds when a mail server is slow, so cap the wait at about 30 seconds (less on a busy form) and treat a timeout, a 429 or a 503 as unknown rather than invalid.