Email verification: confirm an address with a signed link

Updated

Verification proves an address belongs to the person who typed it, and it keeps typos and bots out of your user table. The email carries a single-use link.

When it fires

Right after account creation, before the account is fully active. It should arrive within seconds and be the only email the user receives at that moment. Hold the welcome email until verification completes. See welcome email.

Workflow

  1. Create the user with verified = false.
  2. Generate a random token, store its hash with an expiry.
  3. Email a link containing the raw token.
  4. On click, hash the token from the URL, look it up, check expiry, mark the user verified, delete the token.

Implementation (Python 3, requests)

import hashlib, os, secrets
from datetime import datetime, timedelta, timezone
import requests

def send_verification(db, user):
    token = secrets.token_urlsafe(32)
    db.save_token(
        user_id=user.id,
        token_hash=hashlib.sha256(token.encode()).hexdigest(),
        expires_at=datetime.now(timezone.utc) + timedelta(hours=24),
    )
    link = f"https://app.yourdomain.com/verify?token={token}"

    resp = requests.post(
        "https://api.inboxili.com/api/v1/transactional/send",
        headers={"Authorization": f"Bearer {os.environ['INBOXILI_API_KEY']}"},
        json={
            "to": user.email,
            "from_email": "hello@yourdomain.com",
            "from_name": "Acme",
            "subject": "Confirm your email address",
            "html_body": '<p>Hi {{first_name}}, confirm your address: <a href="{{link}}">Verify email</a>. The link works for 24 hours.</p>',
            "text_body": f"Confirm your address: {link}",
            "template_data": {"first_name": user.first_name, "link": link},
        },
        timeout=10,
    )
    if not resp.ok:
        raise RuntimeError(resp.json().get("error", {}).get("message", resp.text))

Sample email

Subject: Confirm your email address Hi Ada, confirm your address: Verify email. The link works for 24 hours.

Things that go wrong

  • Tokens stored in plain text. Store a hash, same as a password.
  • Link scanners. Some corporate mail gateways open links automatically. Make the GET page show a "Confirm" button that POSTs, so a scanner cannot verify on the user's behalf.
  • No resend path. Add a rate-limited resend that invalidates the previous token.
  • Wrong domain in the link. Build links from configuration, never from the request Host header.

Delivery events

A bounced webhook for a verification email is a strong signal the address is mistyped. Flag the account and prompt for a corrected address.

Deliverability notes

One call to action, no images required. Use the same verified From address every time so recipients and mailbox providers learn it.

Frequently asked questions

Link or code?
A link needs one tap on a phone. A code works when the user opens the email on a different device from the signup page. Many products offer a link and show the code too.
How long should the link last?
24 hours is typical. Always offer a resend.

Send a verification email

Create a workspace, verify a domain, and make your first API call.

Related