Skip to main content

Never block a signup

:::danger The one rule

Signitize must never cost you a real user. If Signitize can't answer (outage, timeout, network error, quota used up), your signup still succeeds. You check that user again later.

:::

Read this page before you go live. It takes about 15 minutes to implement, and it is the difference between "Signitize had a hiccup" and "our signup form was down". This page is the reference for failure handling; the other pages link here.

The pattern: fail open, recheck later​

  1. Call Signitize with a short timeout (5 seconds).
  2. If it answers, use the result: reject throwaway, undeliverable and duplicate addresses.
  3. If it doesn't answer, create the account anyway and mark it pending. The user sees nothing unusual.
  4. A background job rechecks pending users every few minutes until Signitize answers, then marks them passed or flagged.
  5. Hold back what abusers want (free credits, trials, bonuses) until the user is passed. Real users barely notice; abusers gain nothing from an outage.

Two things you should not do:

  • Don't stop the signup when Signitize fails ("fail closed"). Any outage, or any network problem between your server and ours, would take your signup form down with it.
  • Don't just skip the check either. Users created during an outage would never be checked, and throwaway or duplicate accounts would slip through for good.

What to do with each response​

Response from /v1/checkWhat your user seesWhat you doMark the user
200, disposable: true"Please use a permanent email"Reject the signup(not created)
200, deliverable: false"Please check it for typos"Reject the signup(not created)
200, email already used"You already have an account"Reject as a duplicate(not created)
200, all goodNormal signupCreate the account with the canonical emailpassed
400 (not an email address)"Enter a valid email address"Reject the signup(not created)
401 or 422Normal signupCreate the account. Alert your team: your API key or your request is wrong (Errors)pending
429 QuotaExceededErrorNormal signupCreate the account. Alert your team and upgrade: every signup is pending until the 1stpending
429 RateLimitErrorNormal signupCreate the account. Too many calls this second; don't wait for a retrypending
5xx, timeout, network error, anything elseNormal signupCreate the accountpending

Only 200 and 400 ever change what the user sees. Everything else is invisible to them.

Step 1: store the result​

Add three columns to your users table:

ColumnTypeMeaning
email_canonicaltext, unique, nullableThe email returned by Signitize. NULL while the check is pending
email_checktextpassed, pending or flagged
email_flagtext, nullableWhy a user was flagged: disposable, undeliverable, invalid or duplicate

A unique column accepts several NULLs in PostgreSQL, MySQL and SQLite, so pending users don't collide.

Step 2: the signup​

async function checkEmail(email) {
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),
})
if (response.ok) return { status: 'checked', result: await response.json() }
if (response.status === 400) return { status: 'invalid' }
if (response.status === 401 || response.status === 422) alertTeam(`Signitize rejected our request: ${response.status}`)
if (response.status === 429) {
const body = await response.json().catch(() => null)
if (body?.error?.type === 'QuotaExceededError') alertTeam('Signitize monthly quota used up')
}
return { status: 'unavailable' }
} catch {
return { status: 'unavailable' } // timeout or network error
}
}

async function signup(req, res) {
const typed = req.body.email.trim()
const check = await checkEmail(typed)

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

if (check.status === 'checked') {
const { email, disposable, deliverable } = check.result
if (disposable) return res.status(422).json({ error: 'Please use a permanent email address.' })
if (!deliverable) return res.status(422).json({ error: 'This email address cannot receive mail. Please check it for typos.' })
if (await users.findByCanonicalEmail(email)) return res.status(409).json({ error: 'You already have an account.' })
await users.create({ email: typed, emailCanonical: email, emailCheck: 'passed' })
} else {
await users.create({ email: typed, emailCanonical: null, emailCheck: 'pending' })
}

await sendVerificationEmail(typed) // always to the address the user typed
return res.status(201).json({ ok: true })
}

Step 3: recheck pending users​

Run this every 10 minutes (cron, a scheduled task or your job queue). It stops as soon as Signitize still doesn't answer, so an outage doesn't waste requests, and it pauses between calls to stay under your plan's per-second limit.

const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms))

async function recheckPendingSignups() {
const pending = await users.findPending({ limit: 50, oldestFirst: true })

for (const user of pending) {
await sleep(600) // stay under 2 requests per second (Free plan)
const check = await checkEmail(user.email)
if (check.status === 'unavailable') return // still down: try again next run

if (check.status === 'invalid') {
await users.update(user.id, { emailCheck: 'flagged', emailFlag: 'invalid' })
continue
}

const { email, disposable, deliverable } = check.result
const owner = await users.findByCanonicalEmail(email)
const flag = disposable ? 'disposable' : !deliverable ? 'undeliverable' : owner && owner.id !== user.id ? 'duplicate' : null

await users.update(user.id, {
emailCanonical: flag === 'duplicate' ? null : email,
emailCheck: flag ? 'flagged' : 'passed',
emailFlag: flag,
})
}
}

Each recheck that gets a 200 or 400 counts as one check. A 503 is never counted, so rechecking during an outage costs you nothing. A call your client abandons at its own timeout may still be counted if Signitize finishes it.

Step 4: hold back the perks, not the user​

A pending user can do everything a normal new user can, except what abusers sign up for. Grant those when the user becomes passed:

  • free credits, trial periods, signup bonuses, referral rewards
  • sending invitations or messages to other people
  • anything that costs you money per account

When a recheck flags a user:

email_flagSuggested action
disposableKeep the account but no perks. Ask the user to change to a permanent address to unlock them
duplicateNo perks. Review it or merge it with the existing account
undeliverable, invalidThe verification email can't arrive anyway. Delete the account if it is still unverified after a few days

Don't delete flagged users the moment they are flagged: a person may be waiting on the other side.

Test your fallback before launch​

Make Signitize unreachable on purpose and sign up a test user. The signup must succeed and the user must be pending:

  • point your client at an address that refuses connections, such as http://127.0.0.1:9/v1/check, or
  • set the timeout to 1 millisecond.

Then restore the URL, run the recheck job and confirm the user becomes passed.

What Signitize does on its side​

  • Clear failures. An outage answers 503, never a misleading 200, so you always know when to mark a user pending.
  • No charge for outages. A 503 is not counted toward your quota, so rechecks during an outage are free.
  • Two kinds of 429. RateLimitError means "retry in a second"; QuotaExceededError means "your monthly quota is used up". See Rate limits.
  • Partial answers stay safe. If a DNS lookup fails inside a check, Signitize still answers 200 and never marks a real address as undeliverable because of it. See Speed and failure handling.