A one-time code email is the most impatient message your product will ever send. The user is staring at a screen that says “Enter the 6-digit code we sent you”, and every second the email takes is a second they spend wondering whether to give up.
Verification emails also sit right on your security boundary. Get them wrong and you either lock real users out or let attackers in. This guide covers both sides: getting the code into the inbox fast, and making sure only the right person can use it.
Codes or links?
There are two common shapes:
- One-time codes (OTP): a short number such as
482913, typed back into your app. Good for sign-in and two-step verification, and they work when the user reads email on a different device. - Verification links: a long random token in a URL. Good for confirming an email address at sign-up or resetting a password, because one click completes the flow.
Everything below applies to both. The difference is mostly length: links can carry a long token, codes must be short enough to type.
Generate codes securely
Use your language’s cryptographically secure random generator. Math.random() and Python’s random module are predictable and must never be used for security codes.
// Node.js
import { randomInt, createHmac } from 'node:crypto';
const code = String(randomInt(0, 1_000_000)).padStart(6, '0'); // "048213"
// Keyed hash: OTP_SECRET lives in your secrets manager, never in the database.
const codeHash = createHmac('sha256', process.env.OTP_SECRET).update(`${user.id}:${code}`).digest('hex');
# Python
import hashlib
import hmac
import os
import secrets
code = f"{secrets.randbelow(1_000_000):06d}"
# Keyed hash: OTP_SECRET lives in your secrets manager, never in the database.
code_hash = hmac.new(os.environ["OTP_SECRET"].encode(), f"{user_id}:{code}".encode(), hashlib.sha256).hexdigest()
Store only a keyed hash (HMAC), together with the user ID, an expiry time and an attempt counter. A plain SHA-256 isn’t enough: with only a million possible codes, anyone holding the database could try them all in under a second. The HMAC needs a secret kept outside the database, so a database leak alone doesn’t reveal live codes.
Expire fast, limit attempts
A six-digit code has one million possibilities. That is plenty only if attackers can’t keep guessing:
| Control | Recommended value |
|---|---|
| Code lifetime | 10 minutes for sign-in codes; up to 24 hours for email-confirmation links |
| Wrong attempts per code | 5, then the code is invalid |
| Codes sent per address | About 3 per 15 minutes, with a visible cooldown on “Resend” |
| Codes sent per IP address | A higher cap, to stop one attacker cycling through many addresses |
| Use | Single use: delete or mark the code as used the moment it succeeds |
Always compare codes in constant time (crypto.timingSafeEqual in Node.js, hmac.compare_digest in Python), and issuing a new code should invalidate the previous one.
Send it immediately
OTP emails should go out in the same request that asked for them, not in a batch job that runs every minute. With Inbixio that is one call:
await fetch('https://backend.inbixio.com/api/send-email/', {
method: 'POST',
headers: { 'X-API-Key': process.env.INBIXIO_API_KEY, 'Content-Type': 'application/json' },
body: JSON.stringify({
to_email: user.email,
subject: `${code} is your YourApp sign-in code`,
message: otpTemplate({ code, minutes: 10 }),
}),
signal: AbortSignal.timeout(10_000),
});
Inbixio accepts the request and queues delivery straight away, so your endpoint can respond quickly. If the call fails, tell the user right away (“We couldn’t send your code. Please try again in a moment”) instead of leaving them waiting for an email that will never arrive. Handling email API errors lists which errors are worth an automatic retry.
Two delivery tips specific to codes:
- Use a dedicated sender, such as
security@yourco.com, connected as its own server, with an API key pinned to it (re-pin the key if you ever remove that server). Users learn to trust that address, and marketing campaigns can’t affect its reputation. - Authenticate your domain. Codes that land in spam are codes that never arrive. Work through the SPF, DKIM and DMARC checklist.
Write an email that can’t be mistaken for phishing
Your template competes with phishing emails that copy exactly this format. Make the real one easy to recognise:
- Put the code in the subject line, e.g. “482913 is your YourApp sign-in code”. Users can read it from the notification without opening the email, and phone keyboards can suggest it.
- Say what it’s for and how long it lasts: “Use this code to sign in to YourApp. It expires in 10 minutes.”
- Say what to do if it wasn’t them: “If you didn’t try to sign in, you can ignore this email. Your account is safe.”
- Never ask for the code back. State it plainly: “YourApp will never ask you for this code by phone or email.”
- Keep it short, with no marketing, and keep links to an absolute minimum.
A simple, accessible HTML template:
<div style="font-family:system-ui,sans-serif;max-width:480px;margin:auto;color:#111">
<p>Use this code to sign in to YourApp:</p>
<p style="font-size:32px;font-weight:700;letter-spacing:6px;margin:24px 0">482913</p>
<p>It expires in 10 minutes. If you didn't try to sign in, you can ignore this email.</p>
<p style="color:#666;font-size:13px">YourApp will never ask you for this code.</p>
</div>
Inline styles are deliberate. Many email clients strip <style> blocks, so inline CSS is the most reliable way to format email.
Don’t leak which emails have accounts
On a sign-in or “send me a code” screen, show the same response whether or not the address is registered: “If an account exists for that address, we’ve sent a code.” Otherwise your form becomes a free tool for checking which email addresses use your product.
Checklist
- Codes from a cryptographically secure generator; only HMACs stored, with the secret outside the database
- 10-minute expiry, single use, 5 attempts, constant-time comparison
- Rate limits per address and per IP, with a resend cooldown
- Sent immediately, with a clear error if sending fails
- Dedicated, authenticated sender address
- Code in the subject, purpose and expiry in the body, “wasn’t you?” guidance
- Same response whether or not the account exists
Send it with Inbixio
Connect your own SMTP server, create a scoped API key and send from any language with one HTTPS request.