emailverifier.dev
Posts

Should You Verify Email Before or After Account Creation?

| 6 min read | Usama Ejaz
An email passes a verification gate into a pending account, then an inbox confirmation passes a second gate to activate the account.

The best default is to verify the address before creating an active account, then confirm inbox control before activating it. I would run syntax, domain, provider, and mailbox checks while the person can still correct the form. A confirmed failure should stop there. An allowed or inconclusive result can move into a minimal pending record, but that record should not receive trial credits, create a workspace, send invitations, or become a password-recovery target until the confirmation link succeeds.

Verification and confirmation answer different questions. Verification gives you evidence about the address and its mail path. Confirmation proves that someone with access to that inbox completed your challenge at that moment. Neither proves the person’s real-world identity.

The answer is two gates, not one timestamp

A signup flow becomes easier to reason about when each transition has one job:

Transition Evidence required What may happen next
Form to pending Server-side email check plus your signup policy Store a short-lived signup intent and send one confirmation message
Pending to active Single-use, time-limited confirmation token Create or activate the user and enable normal login
Active to entitled Your product’s fraud, billing, or team rules Grant trial credits, paid features, invitations, or other costly actions

The first gate should call the email verification service from your server. The current emailverifier.dev API contract returns a technical status, a recommended action, and the signals behind it. Use the action as the default branch, then apply reason-specific policy from the signals.

  • Block: ask the person to correct the address. Do not create a user just to remember a confirmed syntax, routing, or mailbox failure. If the signal is a known disposable provider, apply the acceptance policy you have already published rather than treating the address as proof of abuse.
  • Review: preserve the uncertainty. A catch-all domain or inconclusive mailbox check can become a pending signup that must confirm before activation.
  • Allow: continue to the pending state. SMTP acceptance is useful evidence, but it is not inbox ownership and does not guarantee delivery.

An HTTP timeout, rate limit, or exhausted credit balance is not an email result. The production result-handling guide separates transport failures from address decisions. For a low-cost waitlist, you may accept the submission into a pending state and confirm it. For a costly free trial, I would delay provisioning until the dependency recovers or the address is confirmed.

OWASP’s email validation and verification guidance says not to activate an account before ownership verification completes. Its anti-automation guidance makes the same practical distinction: verify email before an account is usable, and gate features behind that result. The useful word is usable. Your database may need a pending row, but your product does not need to treat that row as a customer yet.

Make the pending state a real security boundary

A pending signup should be deliberately weaker than a user account. I prefer a separate signup_intents table because it makes accidental activation harder and lets expired attempts be deleted without touching the user table.

This is a reference model, not a migration to paste unchanged:

create table signup_intents (
  id uuid primary key,
  email_original text not null,
  email_key text not null,
  verification_status text not null,
  verification_action text not null,
  verification_signals jsonb not null,
  confirmation_token_hash text not null unique,
  expires_at timestamptz not null,
  consumed_at timestamptz,
  created_at timestamptz not null default now()
);

create unique index one_open_signup_per_email
  on signup_intents (email_key)
  where consumed_at is null;

Keep the address the user entered for display and delivery, and derive email_key with one documented comparison policy. Do not globally remove Gmail dots, plus tags, or provider-specific syntax. If you need the storage rules, the guide to email address case sensitivity covers the boundary between preservation and comparison.

The confirmation endpoint should consume the token and activate the user in one transaction. Store only a hash of the token, reject expired or already-consumed records, and make repeated clicks harmless. The token mechanics are covered in the guide to secure email verification links; the architectural point here is that confirmation promotes state instead of merely flipping a cosmetic flag.

The strongest objection is fair: “If I insert a pending row, haven’t I already created the account?” Only if the rest of your system treats it like one. A pending record must not authenticate, receive a password reset, own billable resources, appear in customer counts, or reserve a valuable entitlement indefinitely. The security boundary is behavior, not the table name.

Set the fallback policy from the cost of being wrong

There is no universal reason to block every uncertain address. There is also no reason to provision every unconfirmed signup. Match the pending state to the consequence:

Product flow Before confirmation After confirmation
Newsletter or waitlist Keep a pending subscription; send only the confirmation message Add the address to the active audience
Standard SaaS signup Keep a pending signup; no normal login or workspace Create the user and empty workspace
Costly free trial No credits, compute, invitations, or scarce resources Apply fraud and velocity rules, then grant the trial
Team invitation Keep the invitation separate from membership Bind the confirmed address to the invited role

Expire abandoned intents, cap resend attempts, and apply rate limits to both signup and confirmation endpoints. Return a generic response when an address is already registered so the flow does not become an account-enumeration endpoint. OWASP’s authentication guidance recommends generic registration responses for exactly that reason.

I would also keep verification evidence out of the permanent user profile unless it serves an operational purpose. Store the decision and the reason long enough to debug the signup policy, mask addresses in logs, and delete expired pending records on a short schedule. Verification signals describe one check at one time. They should not become permanent claims about a person.

So the practical answer is: verify before creating the active user, store only a minimal pending intent when you need to send a challenge, and activate the account only after confirmation. That design catches bad input early without turning an unknown result into a fake verdict, and it keeps expensive product access behind evidence that actually supports it.

Continue reading