Email API

Magic Link Login Emails: Token Design, Expiry and Sending via API

Build passwordless magic link login emails safe against link scanners and replay: token design, expiry, one-click confirmation, and the send API request.

Magic links replace passwords with a one-time URL emailed to the user. They are simple to build and popular with users, but a naive implementation logs people in from a corporate link scanner, or fails when the user opens the link on a different device. This guide covers the token, the URL, the email, the send call, and the preview-fetch problem that catches almost everyone.

Quick answer

Generate a 32-byte random token, store its hash with a 15-minute expiry, email a URL containing the raw token, and when the user opens it show a page with a "Confirm sign-in" button that submits a POST. Only the POST consumes the token. That single design choice defeats link-preview bots and email security scanners that pre-fetch every URL in a message.

Token design

  • Entropy: 32 random bytes (256 bits) encoded as base64url gives a 43-character token that cannot be guessed. Use crypto.randomBytes or the equivalent.
  • Storage: store SHA-256(token), the user id, created_at, expires_at, and consumed_at. Never store the raw token.
  • Expiry: 15 minutes is generous for email delivery and short enough that a forwarded email is useless by the time someone reads it.
  • Single use: set consumed_at on success and reject any token that already has it.
  • Binding: record the requesting IP and user agent for audit, but do not require them to match on click. Users legitimately open on a different device.

The preview-fetch problem

Microsoft Defender Safe Links, Mimecast, Proofpoint, Slack unfurls and even some mobile mail apps open every URL in an email before the human does. If GET /login?token=... consumes the token and creates a session, the scanner logs in, the token is spent, and the real user sees "link expired". Fix it by making the emailed URL land on a page that does nothing on GET except render a button. The button does a POST with the token in the body. Bots do not submit forms. Also set the page to noindex and give it a Cache-Control: no-store header.

The email

Keep it short. Subject: "Your sign-in link for Acme". Body: one sentence, one prominent button, the raw URL as plain text underneath for clients that strip buttons, the expiry in minutes, and the "ignore if this was not you" line. No marketing, no footer of six links; the fewer URLs in the message, the fewer things scanners open and the more clearly the real link stands out.

const res = await fetch("https://api.oqumail.com/api/v1/emails", {
  method: "POST",
  headers: {
    "Authorization": "Bearer " + process.env.OQUMAIL_API_KEY,
    "Content-Type": "application/json"
  },
  body: JSON.stringify({
    from: "login@yourdomain.com",
    to: user.email,
    subject: "Your sign-in link for Acme",
    html: '<p>Click to sign in to Acme:</p><p><a href="' + url + '" style="background:#111;color:#fff;padding:12px 20px;border-radius:6px;text-decoration:none">Sign in</a></p><p>Or paste this link: ' + url + '</p><p>Expires in 15 minutes. If you did not request this, ignore it.</p>',
    text: "Sign in to Acme: " + url + "\n\nExpires in 15 minutes. If you did not request this, ignore it."
  })
});
if (!res.ok) throw new Error("send failed: " + res.status);

The flow, end to end

  1. POST /auth/magic with the email address. Always respond "If that address exists we sent a link", never reveal whether the account exists.
  2. Rate limit: 3 links per address per 15 minutes, and a global cap per IP.
  3. Generate token, store hash, enqueue the send with the URL https://app.yourdomain.com/login/confirm?t=TOKEN.
  4. GET /login/confirm renders a page with a Confirm button (and optionally auto-submits with JavaScript after a short delay, since scanners do not execute it).
  5. POST /login/confirm looks up the hash, checks expiry and consumed_at, creates the session cookie, marks consumed, and redirects to the app.

Sending it reliably

A magic link email that arrives in 40 seconds feels broken. Send from a domain whose SPF, DKIM and DMARC already pass so receivers do not defer you, and send from a background worker so the request returns immediately. With OquMail the domain setup is guided with live DNS checks, the API takes a Bearer key, and every message has a delivery log entry showing the SMTP response from Gmail, Outlook or whoever received it, which is how you prove the link was accepted when someone says it never came.

Pitfalls

  • Putting the token in a URL fragment (#) to hide it from servers. Mail clients often drop fragments.
  • Long tokens wrapped across lines in plain text clients. base64url avoids characters that trigger wrapping; keep the URL on its own line.
  • Logging full request URLs, which writes raw tokens to your access logs. Strip the query string in the logger.
  • Reusing one token for "verify email" and "sign in". Separate purposes, separate tables.
  • No fallback for users who cannot receive email at all. Offer a code or password path for support.

Common questions

Should magic links log the user in on the device that requested them?

Some products do cross-device login: the original tab polls, and when the link is confirmed elsewhere, the original tab signs in. It is a nice touch but adds a websocket or polling endpoint. Start with same-device and add it later.

Are magic links less secure than passwords?

They move the security to the user's mailbox, which for most people is already the recovery path for every password anyway. With a short expiry, single use, and POST-only consumption they are at least as safe as a typical password flow, and immune to credential stuffing.

What should the from address be?

Use a dedicated address such as login@ or security@ on the same domain as your app so users learn to recognise it. Keep a monitored support@ mailbox on that domain too; replies to login emails are often people who are locked out.

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

Ready for business email on your domain?

Up to 15 free mailboxes, guided DNS, webmail, and a transactional API — start in minutes.

Create your free workspace