Skip to main content

Check signup emails in Next.js

This guide adds Signitize to a Next.js (App Router) signup form with a Server Action. The API key stays on the server.

1. Add your key​

.env.local
SIGNITIZE_API_KEY=sk_live_...

Don't prefix it with NEXT_PUBLIC_: that would ship it to the browser. Get a free key if you don't have one.

2. The check​

lib/signitize.ts
import 'server-only'

export type EmailCheck =
| { status: 'checked'; email: string; disposable: boolean; deliverable: boolean }
| { status: 'invalid' }
| { status: 'unavailable' }

export async function checkEmail(email: string): Promise<EmailCheck> {
try {
const response = await fetch('https://api.signitize.io/v1/check', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.SIGNITIZE_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ email }),
signal: AbortSignal.timeout(5000),
cache: 'no-store',
})
if (response.ok) return { status: 'checked', ...(await response.json()) }
if (response.status === 400) return { status: 'invalid' }
return { status: 'unavailable' }
} catch {
return { status: 'unavailable' }
}
}

unavailable covers timeouts, outages, a wrong key and a used-up quota. Log or alert on 401, 422 and 429 so you notice them; Errors lists every status.

Or install the signitize package (npm install signitize): new Signitize().safeCheck(email) returns the same three outcomes, with the result in outcome.result. A runnable app is in the Next.js example.

3. The Server Action​

app/signup/actions.ts
'use server'

import { checkEmail } from '@/lib/signitize'
import { db } from '@/lib/db'

export async function signup(_state: { error?: string }, formData: FormData) {
const typed = String(formData.get('email') ?? '').trim()
const check = await checkEmail(typed)

if (check.status === 'invalid') return { error: 'Enter a valid email address.' }

if (check.status === 'checked') {
if (check.disposable) return { error: 'Please use a permanent email address.' }
if (!check.deliverable) return { error: 'This email address cannot receive mail. Please check it for typos.' }
if (await db.user.findUnique({ where: { emailCanonical: check.email } })) {
return { error: 'You already have an account.' }
}
await db.user.create({ data: { email: typed, emailCanonical: check.email, emailCheck: 'passed' } })
} else {
await db.user.create({ data: { email: typed, emailCanonical: null, emailCheck: 'pending' } })
}

await sendVerificationEmail(typed)
return {}
}

emailCanonical is a unique, nullable column. The example uses Prisma; any database works the same way.

4. The form​

app/signup/page.tsx
'use client'

import { useActionState } from 'react'

import { signup } from './actions'

export default function SignupPage() {
const [state, action, pending] = useActionState(signup, {})

return (
<form action={action}>
<input name="email" type="email" required autoComplete="email" />
{state.error && <p role="alert">{state.error}</p>}
<button disabled={pending}>Sign up</button>
</form>
)
}

5. Recheck pending users​

Users created while Signitize was unavailable are pending. Recheck them every few minutes (a cron route, Vercel Cron or your job queue) and hold back free credits until they are passed. The recheck job is in Never block a signup.

Using an auth library?​

The same checkEmail works inside the signup step of Auth.js, Better Auth or your own handler: call it before the user row is created. For Clerk, see the Clerk guide.