How to Send OTP and Verification Code Emails via API (Done Right)
Send OTP verification code emails that arrive fast and cannot be replayed: code length, expiry, single use, subject lines, text fallback, and the API call.
A verification code email has one job: get a short code in front of the user within seconds, and make sure that code cannot be guessed or reused. Most OTP problems are not delivery problems at all, they are design mistakes like ten-minute expiries, six-digit codes with unlimited attempts, or HTML-only bodies that some clients render as blank. Here is the full recipe, with a working send call.
Quick answer
Generate a random 6-digit code with a cryptographic random source, store only its hash with a 5-minute expiry and an attempt counter, email it with the code in the subject line and in a plain text body, and mark it used the moment it verifies. Send the email from a background job so the signup form returns instantly, and send it from your own domain through an authenticated API so it lands in the inbox rather than spam.
Designing the code itself
- Length: 6 digits is the accepted balance between typing effort and guessability (one million combinations). Use 8 digits only for high-value actions like changing a payout account.
- Randomness: use crypto.randomInt in Node, secrets.randbelow in Python, or random_int in PHP. Never Math.random, and never derive the code from a timestamp.
- Storage: store a SHA-256 hash of the code, not the code. If your database leaks, nobody can log in with old rows.
- Attempts: allow 5 wrong guesses, then invalidate the code and require a fresh one. Without this, 6 digits is brute-forceable.
- Expiry: 5 minutes for login, 15 minutes for email-address verification where people may be switching devices. Longer than that and the code is a liability.
- Single use: delete or flag the row on success. A verified code must never verify twice.
Writing the email
Put the code in the subject line, for example "483 921 is your Acme code". Mobile notifications show the subject, so the user never opens the message. Repeat the code in the first line of the body, in large monospace type so it is easy to read across the room. State the expiry in minutes, not as a timestamp, and add one sentence for the case where the user did not request it: "If you did not try to sign in, you can ignore this email." Do not include links in an OTP email; a code email with a login link teaches users to click links in security emails.
The send request
The whole email is one POST. Use the text field as well as html so terminal mail clients, watches and screen readers all get the code:
curl -X POST https://api.oqumail.com/api/v1/emails \
-H "Authorization: Bearer $OQUMAIL_API_KEY" \
-H "Content-Type: application/json" \
-d '{"from":"security@yourdomain.com","to":"user@example.com","subject":"483921 is your Acme sign-in code","html":"<p style=\"font-size:32px;font-family:monospace;letter-spacing:4px\">483 921</p><p>This code expires in 5 minutes. If you did not request it, ignore this email.</p>","text":"Your Acme sign-in code is 483921. It expires in 5 minutes. If you did not request it, ignore this email."}'Step by step in your app
- User submits their address. Rate limit this endpoint by address and by IP (for example 3 codes per address per 10 minutes) so you are not turned into a spam cannon.
- Generate the code, hash it, and insert a row with expires_at, attempts = 0, and used = false.
- Enqueue a job that calls the OquMail send API with the request above. Do not call the API inside the HTTP handler.
- On verify, look up the newest unused row for that address, check expiry, compare hashes, increment attempts on failure.
- On success set used = true, create the session, and delete any other pending codes for that address.
Deliverability specifics for OTP mail
Speed matters more here than anywhere else. Send from a domain with SPF, DKIM and DMARC already passing, because a first-time sender with missing authentication is exactly what Gmail greylists, and a 4-minute greylisting delay on a 5-minute code is a support ticket. OquMail walks you through those three records with live DNS verification when you add the domain, and every send gets a per-message delivery log with the receiving server's SMTP response, so "I never got my code" can be answered with facts.
Mistakes that break OTP emails
- Sending the code as an image or inside a button. Copy-paste must work.
- Expiring codes when a new one is requested but not telling the user, so the code in their inbox from 30 seconds ago silently fails.
- Using noreply@ with no monitored mailbox. Keep support@ on the same domain in a real inbox; OquMail gives you both on the free plan.
- Logging the plaintext code in application logs.
- Sending from a different domain than the one users signed up on. Mismatched brands look like phishing.
Common questions
Should the OTP go in the subject line?
Yes. It is the biggest single improvement you can make to conversion, because users read it from a lock-screen notification. Some teams worry about shoulder surfing; in practice the code is useless without the password or device it pairs with.
Email OTP or SMS OTP?
Email is cheaper, works worldwide without carrier deals, and is easier to audit through delivery logs. SMS is faster on phones without a mail app. Many products use email as the default and offer SMS as a fallback for accounts that have a verified number.
How many characters should a verification code have?
Six digits with a 5-attempt cap is the standard. Alphanumeric codes look stronger but produce typos (0 versus O, 1 versus l), which costs more logins than it saves.
Free business email on your own domain
OquMail gives you up to 15 mailboxes on your domain — free — with guided SPF/DKIM/DMARC, webmail, IMAP/SMTP for any mail app, and a send API. Most teams are live in under fifteen minutes. Start at oqumail.com.
Get started free