Email Verification: Six-Digit Code vs Confirmation Link (2026)
Compare six-digit email OTP codes vs confirmation and magic links for verification and passwordless login—decision table, phishing and deliverability, one POST /emails.
The decision most products make twice
Almost every SaaS or AI app eventually sends the same proof: “this mailbox belongs to the person using our product.” Two shapes dominate—six-digit verification codes and confirmation / magic links—and teams often ship the first one they copy from a tutorial, then regret it when mobile UX, phishing reports, or corporate filters show up.
Rovela’s role is the same either way: deliver a transactional message with POST /emails. Your application generates the secret, stores a hash, enforces expiry and single-use, and redeems what the user types or clicks. There is no separate Rovela “OTP product” or “magic link product”—only the send API plus the security patterns in the docs.
This post is a comparison: when codes win, when links win, and how deliverability and client quirks tip the scale. Full recipes live in the siblings and docs linked below—use this page to choose, then deep-link.
What “code” and “link” actually mean
Six-digit code (OTP / email verification code)
- App generates a short numeric (or alphanumeric) one-time value with a CSPRNG.
- Store a keyed hash (HMAC), expiry, and attempt counter—not the raw code (Email verification codes).
- User types the code into your UI (or reads it from a push preview / subject line).
- Implementation deep-dives: Sending OTP by email API, Email OTP security.
Confirmation or magic link
- App generates a long unguessable token (for example 32 random bytes,
base64url), stores a hash, short expiry, single-use consume-on-redeem (Password reset links—same token shape). - Email carries an absolute URL on your origin; the user taps or clicks.
- Variants: email ownership confirm, passwordless magic login, reset, invite. Deep-dives: Email verification links, Magic login email links, Secure password reset email, Next.js magic link.
Both are “send verification email API” problems from the ESP’s point of view: one HTTP send, html + text, verified domain, idempotent retries.
Decision table
| Criterion | Six-digit code | Confirmation / magic link |
|---|---|---|
| UX friction | Type 6 digits (or paste) | One tap / click |
| Mobile | Numeric keyboard; works when inbox and app are separate apps | Great if mail client opens your HTTPS URL; awkward if user must switch back manually |
| Desktop | Copy/paste or retype from another tab | Usually fastest when inbox is already open |
| Phishing / social engineering | Users trained to type codes into your UI; still vulnerable to prompt injection (“enter the code here”) | Users trained to click; easier to spoof “verify” buttons; educate “we never ask for the raw token outside this flow” |
| Email client rewriting | Rarely affects digits in body/subject | Proxies, safe-link scanners, and click tracking can wrap or prefetch URLs |
| Accessibility | Some users struggle with cross-device typing; screen readers handle digits well if marked up | Full URL in text helps; focus on clear link text in HTML |
| SMS fallback | Easy to mirror the same code channel | Links work in SMS too, but long URLs wrap badly; prefer short codes for SMS |
| Rate limits / brute force | Tiny space → HMAC + attempt caps + resend throttles are mandatory | Huge token space; still rate-limit issuance and redeem |
| Expiry | Often 5–15 minutes | Ownership confirm may be hours; login/reset often 15–60 minutes |
| Single-use | Delete (or rotate) on success | Consume-on-redeem in one DB statement |
| Deliverability / scanners | Short body; code-in-subject helps previews; less “clickbait link” risk | Longer URLs; prefetch bots may hit the link—design redeem to be safe on GET or require an explicit confirm step |
| Shared / kiosk devices | Better: no durable “logged-in by click” surprise if you only verify presence | Riskier for magic login on shared machines unless session UX is careful |
When codes win
Prefer a six-digit (or short alphanumeric) code when:
- High-risk step-up — “prove you can read this inbox right now” before a sensitive action (export, billing change, new device).
- Shared or supervised devices — a click-to-login link can silently open a session; typing a code into your app keeps intent clearer.
- Users who distrust links — finance, healthcare, and security-conscious audiences often refuse to click.
- Cross-channel parity — you already (or will) send the same code over SMS/voice; one mental model.
- App-centric mobile flows — user stays in your app; mail is only a side channel for digits (and optionally the subject line).
Start from the official verification-code example and harden with Email OTP security.
When links win
Prefer a confirmation or magic link when:
- Passwordless login on personal devices — one tap from inbox to session (Magic login email links).
- Email ownership / double opt-in — durable “yes, this address is mine,” not a 10-minute presence check (Email verification links).
- Lower friction desktop onboarding — inbox already open in another tab.
- Email change — send the link to the new address; only swap after redeem.
- Password reset — long token in a URL is the documented pattern (Secure password reset).
Hybrid patterns (use carefully)
Honest hybrids show up in production:
- Code + link in one email — “Enter
482913or confirm in browser.” Useful when you cannot predict device class. Cost: two secrets to issue, rate-limit, and expire; keep purposes clear (same session intent). - Link for ownership, code for login — common and clean: verify once with a link, then OTP for recurring sign-in or step-up.
- Link that lands on a page asking for a code — usually worse UX; avoid unless compliance forces it.
Do not treat hybrids as a Rovela feature—they are app design choices that still send through one POST /emails.
Deliverability and client quirks that change the choice
Security mail fails differently from newsletters. The practical differences:
- Codes are short. Less HTML chrome, easier to keep text/html aligned, and putting the code in the subject helps lock-screen previews (verification-code docs). Skip subject codes on highly shared screens.
- Links invite scanners. Corporate “safe links,” antivirus fetchers, and some clients prefetch
GETURLs. If redeem is a bare GET that logs the user in, prefetch can burn the token or create a session. Prefer consume-on-confirm POST, or make GET show “Continue” without side effects until an explicit action. - Click tracking rewrites URLs. Password-reset docs warn that tracking wraps links through a tracking host; it still works, but security tokens should go straight to your app—use a sending domain/subdomain with click tracking off for auth mail (password-reset example).
- Always send
textwith the full URL or code. Filters that strip HTML links leave the plain-text part. - Reputation isolation. Send auth from a subdomain (
security@mail.example.com) so marketing never shares fate with OTP and magic login (Why OTP and magic-link emails go to spam).
One send path: POST /emails
Whether the body is digits or an <a href>, the wire format is identical. From a verified domain, with a sending_access key (Authentication, Quick Start):
curl -X POST https://api.rovela.dev/emails \
-H "Authorization: Bearer re_your_sending_key" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: verify-user_123-2026-11-04" \
-d '{
"from": "Acme <security@mail.acme.com>",
"to": "alice@example.com",
"subject": "Confirm your Acme email",
"html": "<p>Your code is <strong>482913</strong> (10 minutes).</p><p>Or <a href=\"https://app.acme.com/verify?token=…\">confirm in the browser</a>.</p>",
"text": "Your code is 482913 (10 minutes).\n\nOr confirm: https://app.acme.com/verify?token=…"
}'
Notes that apply to both shapes:
200means queued, not inbox placement—use message events or webhooks (API: emails).Idempotency-Key(≤256 chars, 24h) prevents double-sends when your worker retries.- Suppressions: a
400“recipient is suppressed” means do not pretend you sent a code or link—ask for another address. - Volume and plan limits: see Pricing; this article invents no quotas.
For AI-product context (codes, invites, login links as a set), see Transactional email for AI apps.
Suggested default for 2026 products
| Flow | Default | Why |
|---|---|---|
| Sign-up email confirm | Link (24h-class expiry) | Ownership, low friction on desktop |
| Passwordless login | Magic link or code | Link for consumer apps; code if shared devices / high distrust of links |
| Step-up / sensitive action | Code | Presence proof without durable session via email click |
| Password reset | Link | Matches documented long-token pattern |
| Invite accept | Link | Action is “join this org,” not “type six digits” |
Revisit when support tickets cluster around “code never arrived” vs “I won’t click that”—often a deliverability issue (spam guide), not a wrong UX primitive.
What this post is not
- Not a full OTP implementation (see send-otp + docs).
- Not a full magic-login or verify-link tutorial (see those blogs).
- Not a claim that Rovela stores or validates your codes/tokens.
- Not China-domestic vendor comparison or invented pricing.
Next steps
- Choose with the table above.
- Implement app-side secrets: verification codes or password-reset / link pattern.
- Deliver with POST /emails · Quick Start.
- Harden deliverability: OTP / magic-link spam.
- Limits: Pricing.
Bottom line: six-digit codes optimize for presence and distrust of links; confirmation links optimize for friction and durable ownership. Same send verification email API either way—Rovela queues and authenticates the message; your app owns the cryptography.