Magic Link Authentication: How to Send Passwordless Login Emails

Magic Link Authentication: Send Passwordless Login Emails
Vitalii Piddubnyi Vitalii Piddubnyi 09 june 2026, 06:25 449
For beginners

Every login screen asks you the same thing, to prove it's you. For years, the correct answer was a password no one else knows. But passwords get reused, phished, forgotten, and dumped in breach lists, and each one you use has a chance of getting leaked. Another annoying scenario is when you click "Forgot password?" and end up with a password reset email that never reaches the inbox.

Magic links take a different route. Instead of requiring the user to remember a password, you email them a one-time link that signs them in when they click it. No password to type, store, or leak. 

This guide will show what magic links are, how the authentication flow actually works under the hood, and how to send them reliably with UniOne over the Web API or SMTP, including a working Node.js demo you can run end to end.

A magic link is a one-use URL emailed to a user that allows them in without a password. The link includes a unique token, and when the user clicks it, your backend checks the token and starts a session.

This is what people mean by passwordless authentication. You replace "something you know" (a password) with "something you have" (access to a mailbox). The same idea powers tools you already use, such as Slack, Notion, and Substack, which all offer the option to sign in this way, and we’ve highlighted some of the examples for such flows in our more detailed article explaining magic links and how they work

It helps to see passwordless login next to the two methods it competes with.

 

Passwords

OTP codes

Magic links

What the user does

Types in a memorized secret

Copies a short code from email/SMS

Clicks a link in their email

Main strength

Familiar, works for offline apps

Quick, no link to misroute

Lowest friction, nothing to type

Main weakness

Reused, phished, forgotten passwords

Code can be read aloud to an attacker

Needs fast, deliverable email

Server stores

Password hash, indefinitely

Short-lived code

Short-lived token hash

Reset burden

High (forgotten password flows)

None

None

To learn more about sending OTP emails via UniOne, check out this guide.

Why startups are switching to passwordless login

The pitch for magic link authentication is mostly about removing friction at the moment a user decides that your product is worth their attention.

Benefits

Onboarding gets faster because there's no "create a password, retype it, meet these four rules". The user types in an email, and they're in. That removes the highest-friction field from a signup form, leading to fewer abandoned signups. 

It also eliminates password-reset traffic entirely, because there's no password to reset. This removes one of the most common support requests and one of your highest-volume transactional emails. 

Logins become mobile-friendly, since tapping a link beats typing a strong password on a phone, and the attack surface shrinks. No stored passwords means no password database to leak and nothing for credential-stuffing attacks to try.

Use case

Magic links shine in SaaS products and B2B platforms where logins are occasional and security matters.

They're also a clean fit for mobile apps and CMS-based projects. The one place to think twice is high-frequency consumer apps where users log in many times a day, and waiting for an email would annoy them. There, passkeys or persistent sessions may serve better.

Here's the magic link authentication flow from the user's first click to a live session:

  1. A user enters their email on your login screen and submits.
  2. Your backend generates a token. This is a long, cryptographically random string.
  3. You store the token securely with an expiry time, linked to that email. (Store a hash of it, not the raw value.)
  4. UniOne sends the email containing the link, with the token baked into the URL.
  5. The user clicks the link, which opens a verification route in your app.
  6. Your backend validates the token: does it exist, is it valid, has it been used before?
  7. A session is created, the token is marked as used, and the user is logged in.

The token's lifecycle

The token is the entire security model, so its properties matter:

  • It expires fast: A short window, about 15 to 30 minutes, is a reasonable default as it limits how long a leaked link remains dangerous. Shorter expiry reduces risk but can frustrate users who don't check email immediately.
  • It's single-use: The first successful click consumes it. Any subsequent click does nothing.
  • It's unguessable: Current guidance is at least 256 bits of cryptographically random data, far beyond what brute force can reach.
  • It's stored hashed: If your database leaks, attackers get hashes, not live tokens. The same reasoning is behind hashing passwords.

How does this prevent replay attacks?

A replay attack is when someone captures a valid link and reuses it. Single-use tokens battle this directly. Once the legitimate click consumes the token, any replayed copy is already dead. Add the short expiry time and HTTPS-only links (so the token never travels in plaintext), and the window for abuse is tiny.

Why transactional email infrastructure matters for magic links

With magic links, the email sent to the user acts as the initial login button. If the email is slow to arrive, the user sits on a "check your inbox" screen, wondering if your product is broken. If it lands in spam, they can't log in at all.

Deliverability and sender reputation

A few things emerge from treating email as the login button. A delayed email equals a broken login; even a 10 to 15 seconds delay can feel like a failure to someone mid-signup, so delivery speed is important. 

Spam placement blocks authentication outright. An auth email landing in spam is a user locked out of your product. And your sender reputation determines whether your auth email lands in spam, because mailbox providers route your mail based on your domain's standing, so deliverability directly affects conversion. 

This is why authentication mail should ride on infrastructure built for transactional traffic. If you want to go deeper on inbox placement, UniOne has a full guide on email deliverability.

SPF, DKIM, and DMARC

Reliable auth mail also depends on authenticating your sending domain with three dedicated DNS records:

  • SPF lists which servers may send on behalf of your domain.
  • DKIM cryptographically signs your email so recipients can verify it hasn't been altered.
  • DMARC tells receivers what to do when SPF or DKIM fails, and provides reporting.

Another two practices keep the auth mail reliable: 

  • Send from a dedicated subdomain, like auth.yourapp.com
  • Keep transactional and marketing traffic isolated, so a promotional campaign doesn't drag down the deliverability of the emails people need to log in.

By the end of this section, you'll have a small Node.js app that sends a real-life magic link email through UniOne, verifies the click, and logs the user in without a password.

For an authentication system, the Web API is almost always the right integration choice over SMTP. It gives you:
Structured error handling;

  • A returned job_id you can trace in your logs;
  • Metadata you can attach to each send;
  • An idempotence_key to stop accidental duplicate emails;
  • Throughput to scale as signups grow. 

Check out UniOne's full API method reference in Email API documentation.

Prerequisites 

  • Node.js v18 or newer installed.
  • A UniOne account with a verified sender domain. 
  • Your UniOne API key, from the account settings page.

Before getting started with the implementation, do the following:

  • Sign up for a free UniOne account here.
  • Add and verify your domain name. For a detailed procedure, refer to our guide on Setting up the domain’s DNS records. Alternatively, as a new user, you can use the free, pre-verified sandbox domain, which is a temporary domain for testing your email sending functionality before connecting your production domain.

Step 1: Create the project

Open a terminal window and run these commands one at a time:

mkdir magic-link-demo && cd magic-link-demo
npm init -y
npm pkg set type=module
npm install express dotenv cookie-parser

That last line installs the only three dependencies we need. Now, create a file called .env in the project folder for your secrets:

UNIONE_API_KEY=your-unione-api-key-here
FROM_EMAIL=auth@yourapp.com
FROM_NAME=Your App
APP_URL=http://localhost:3000

  • UNIONE_API_KEY: To get your API key, navigate to Settings -> SMTP Configuration. Click API key at the top of the page and copy your key into the project.
  • FROM_EMAIL: For this tutorial, you can use the free, pre-verified sandbox domain provided by UniOne, for example, john@sandbox-7046866-f66e37.unionemailer.com.

Step 2: Generate and store the token

This is the security core of the system. Create a lib/tokens.js file, then paste in the code below:

// lib/tokens.js
import crypto from "node:crypto";

const TOKEN_TTL_MS = 15 * 60 * 1000; // 15-minute expiry

// Demo store only -- resets when the server restarts.
// In production, use Redis (with native TTL) or a database table.
const loginTokens = new Map(); // tokenHash -> { email, expiresAt, used }

function hashToken(rawToken) {
  return crypto.createHash("sha256").update(rawToken).digest("hex");
}

export function createLoginToken(email) {
  // 32 random bytes = 256 bits of entropy
  const rawToken = crypto.randomBytes(32).toString("base64url");
  const tokenHash = hashToken(rawToken);

  loginTokens.set(tokenHash, {
    email,
    expiresAt: Date.now() + TOKEN_TTL_MS,
    used: false,
  });

  // The RAW token goes in the email link. Only the HASH is stored.
  return { rawToken, ttlMinutes: TOKEN_TTL_MS / 60000 };
}

export function consumeLoginToken(rawToken) {
  const tokenHash = hashToken(rawToken);
  const record = loginTokens.get(tokenHash);

  if (!record) return { ok: false, reason: "invalid" };
  if (record.used) return { ok: false, reason: "already_used" };
  if (Date.now() > record.expiresAt) {
    loginTokens.delete(tokenHash);
    return { ok: false, reason: "expired" };
  }

  record.used = true; // single use -- a second click does nothing
  return { ok: true, email: record.email };
}

The raw token is what travels in the email link, but only its SHA-256 hash is ever stored. If your database is leaked, an attacker would get only hashes they can't practically reverse into working links. The used flag guarantees single use, and expiresAt enforces the 15-minute window.

You'll also need a place to store sessions once a login succeeds. Create lib/sessions.js file with the code below:

// lib/sessions.js
import crypto from "node:crypto";

const sessions = new Map(); // sessionId -> { email, createdAt }

export function createSession(email) {
  const sessionId = crypto.randomBytes(32).toString("base64url");
  sessions.set(sessionId, { email, createdAt: Date.now() });
  return sessionId;
}

export function getSession(sessionId) {
  return sessions.get(sessionId) || null;
}

export function destroySession(sessionId) {
  sessions.delete(sessionId);
}

This file keeps a user logged in after they click the magic link, so they aren't re-verified by email on every page. 

createSession generates a long random ID, stores it server-side against the user's email, and returns the ID (which server.js puts in an HttpOnly cookie); the cookie holds only that meaningless ID, never the email. 

getSession looks up the ID from an incoming request to confirm who's logged in, and destroySession deletes the record, which is logout. It's an in-memory Map with no real expiry, so it resets on restart, which is fine for the demo, but production would use Redis or a database.

Step 3: Send the email through UniOne Web API

This is where UniOne comes in. Create lib/email.js file, then paste in the code below:

// lib/email.js
const UNIONE_ENDPOINT =
  "https://api.unione.io/en/transactional/api/v1/email/send.json";

export async function sendMagicLinkEmail({ to, link, expiresInMinutes, idempotenceKey }) {
  const payload = {
    message: {
      recipients: [
        {
          email: to,
          // Template variables UniOne swaps into the body below
          substitutions: {
            magic_link: link,
            expires_in: String(expiresInMinutes),
          },
          // Optional metadata travels back to you in webhooks
          metadata: { event: "magic_link_login" },
        },
      ],
      from_email: process.env.FROM_EMAIL,
      from_name: process.env.FROM_NAME,
      subject: "Your login link",
      track_links: 0, // never rewrite an auth link
      track_read: 0,
      tags: ["magic-link", "auth"],
      idempotence_key: idempotenceKey,
      body: {
        plaintext:
          "Sign in with this link: {{magic_link}}\n\n" +
          "It expires in {{expires_in}} minutes. " +
          "If you didn't request it, ignore this email.",
        html:
          '<div style="font-family:system-ui,sans-serif;max-width:480px;margin:0 auto">' +
          "<h2>Sign in to Your App</h2>" +
          "<p>Click the button below to sign in. No password needed.</p>" +
          '<p><a href="{{magic_link}}" style="display:inline-block;padding:12px 20px;' +
          'background:#4f46e5;color:#fff;text-decoration:none;border-radius:8px">Sign in</a></p>' +
          '<p style="color:#666;font-size:14px">This link expires in {{expires_in}} minutes. ' +
          "If you didn't request it, ignore this email.</p>" +
          "</div>",
      },
    },
  };

  const res = await fetch(UNIONE_ENDPOINT, {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      Accept: "application/json",
      "X-API-KEY": process.env.UNIONE_API_KEY,
    },
    body: JSON.stringify(payload),
  });

  const data = await res.json();
  if (!res.ok || data.status !== "success") {
    throw new Error(`UniOne send failed: ${JSON.stringify(data)}`);
  }

  // job_id lets you trace this exact email in logs and webhooks
  return data.job_id;
}

Four details in that payload are worth a closer look, because they are what differs a demo code from a production auth integration:

  • substitutions are UniOne's template variables. We pass magic_link and expires_in, and UniOne inserts them into every {{magic_link}} and {{expires_in}} placeholder in the body. The substitution syntax uses double curly braces around the variable name.
  • track_links: 0 turns off click tracking, and for an auth email this is non-negotiable. Link tracking converts your URL into a redirected one, which means your single-use token can be consumed by a link scanner or logged somewhere you don't want it. Always send authentication links untracked.
  • idempotence_key is UniOne's safeguard against duplicate sends which usually occur when an app retries an API request. If UniOne receives two requests with the same key, the second one returns the result of the first without sending another email. We tie it to the token so a retried request for the same login can't be sent twice.
  • job_id comes back on success. A successful send returns a JSON structure with status: "success", a job_id, and a list of accepted emails. Store it next to your login attempt so you can check whether that exact email was delivered, bounced, or marked as spam.

Step 4: Verify the click and create a session

Now create server.js, which ties everything together: the request route, the verify route, your protected page, and logout.

// server.js
import express from "express";
import crypto from "node:crypto";
import cookieParser from "cookie-parser";
import "dotenv/config";

import { createLoginToken, consumeLoginToken } from "./lib/tokens.js";
import { createSession, getSession, destroySession } from "./lib/sessions.js";
import { sendMagicLinkEmail } from "./lib/email.js";

const app = express();
app.use(express.json());
app.use(cookieParser());
app.use(express.static("public"));

const APP_URL = process.env.APP_URL || "http://localhost:3000";

// Basic rate limit: one request per email per minute
const lastRequest = new Map();
function rateLimited(email) {
  const now = Date.now();
  if (now - (lastRequest.get(email) || 0) < 60_000) return true;
  lastRequest.set(email, now);
  return false;
}

// 1) User requests a magic link
app.post("/auth/magic-link", async (req, res) => {
  const email = (req.body.email || "").trim().toLowerCase();
  if (!email.includes("@")) {
    return res.status(400).json({ error: "Enter a valid email." });
  }

  // Respond identically even when rate-limited, so we don't reveal
  // which emails have accounts.
  if (rateLimited(email)) return res.json({ ok: true });

  try {
    const { rawToken, ttlMinutes } = createLoginToken(email);
    const link = `${APP_URL}/auth/verify?token=${rawToken}`;
    const idempotenceKey = crypto
      .createHash("sha256")
      .update(rawToken)
      .digest("hex");

    const jobId = await sendMagicLinkEmail({
      to: email,
      link,
      expiresInMinutes: ttlMinutes,
      idempotenceKey,
    });

    console.log(`Magic link sent to ${email} (job_id: ${jobId})`);
    res.json({ ok: true });
  } catch (err) {
    console.error(err);
    res.status(502).json({ error: "Couldn't send the login email. Try again." });
  }
});

// 2) User clicks the link
app.get("/auth/verify", (req, res) => {
  const result = consumeLoginToken(req.query.token);
  if (!result.ok) {
    return res
      .status(400)
      .send(`This link is ${result.reason.replace("_", " ")}. Request a new one.`);
  }

  const sessionId = createSession(result.email);
  res.cookie("session", sessionId, {
    httpOnly: true,
    sameSite: "lax",
    secure: process.env.NODE_ENV === "production", // HTTPS-only in prod
    maxAge: 7 * 24 * 60 * 60 * 1000,
  });
  res.redirect("/dashboard");
});

// 3) A protected page
function requireAuth(req, res, next) {
  const session = getSession(req.cookies.session);
  if (!session) return res.redirect("/");
  req.user = session;
  next();
}

app.get("/dashboard", requireAuth, (req, res) => {
  res.send(`<!doctype html><meta charset="utf-8">
    <div style="font-family:system-ui;max-width:480px;margin:80px auto">
      <h1>You're signed in</h1>
      <p>Logged in as <strong>${req.user.email}</strong> -- no password required.</p>
      <form method="POST" action="/logout"><button>Log out</button></form>
    </div>`);
});

app.post("/logout", (req, res) => {
  destroySession(req.cookies.session);
  res.clearCookie("session");
  res.redirect("/");
});

app.listen(3000, () => console.log(`Running at ${APP_URL}`));

This is the main server that wires the whole flow together. It sets up Express, pulls in the token, session, and email helpers, and serves the login page from public. 

The /auth/magic-link route handles a login request. It cleans up the email, rate-limits to one link per email a minute (always returning ok so no one can check whether an account for this email actually exists), creates a token, builds the verify link, and hands it to UniOne for sending, logging the returned job_id. 

The /auth/verify route runs when the user clicks that link: it consumes the one-time token, and on success creates a session and drops the ID into an HttpOnly cookie before redirecting to the dashboard. 

Finally, requireAuth gates the protected /dashboard by checking that cookie's session, /logout deletes the session and clears the cookie, and app.listen starts the server on port 3000.

Step 5: Create login page

Create a public/index.html folder, containing the code below:

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1" />
  <title>Sign in</title>
  <style>
    body { font-family: system-ui, sans-serif; max-width: 420px; margin: 80px auto; padding: 0 16px; }
    input, button { font-size: 16px; padding: 10px 12px; width: 100%; box-sizing: border-box; }
    button { margin-top: 12px; cursor: pointer; }
    .msg { margin-top: 16px; color: #166534; }
  </style>
</head>
<body>
  <h1>Sign in</h1>
  <p>Enter your email and we'll send you a one-time login link.</p>
  <input id="email" type="email" placeholder="you@example.com" />
  <button id="send">Email me a link</button>
  <p class="msg" id="msg" hidden></p>

  <script>
    const btn = document.getElementById("send");
    const msg = document.getElementById("msg");
    btn.addEventListener("click", async () => {
      btn.disabled = true;
      const email = document.getElementById("email").value;
      const res = await fetch("/auth/magic-link", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ email }),
      });
      msg.hidden = false;
      msg.textContent = res.ok
        ? "Check your inbox for your login link."
        : "Something went wrong. Please try again.";
      btn.disabled = false;
    });
  </script>
</body>
</html>


This is what the users see during login.

Now, to test our application, run the command below:

node server.js

Open http://localhost:3000 in your browser, and you should see the demo app rendered. Type an email you can check, and click Email me a link.

Magic Link Authentication - Sign in page

Now, check your inbox. You should see the auth email arrive within a few seconds:

Magic Link Authentication - Sign in email

Click Sign in, and you’ll be automatically logged in without a password:

Magic Link Authentication - Sign in Success

Click the same link a second time, and it's refused, because the token was consumed on the first click. 

Tracking delivery with webhooks

Sending the email is only half the job. For auth traffic, you also want to know what happened to it. UniOne webhooks push delivery events to a URL you control. A callback typically carries the job_id, the recipient email, the status, the event time, and any metadata you attached at send time, so you can match an event back to a specific login attempt. 

A job_id is created every time someone clicks Email me a link. You can see this in your terminal:

Magic Link Authentication - Email Job ID

A minimal callback handler looks like this:

app.post("/webhooks/unione", express.json(), (req, res) => {
  for (const event of req.body.events || []) {
    const { event_data } = event;
    console.log(`${event_data.email}: ${event.event_name} (job_id ${event_data.job_id})`);
    // do something useful here, e.g. on a hard bounce, flag the address and offer another login method
  }
  res.sendStatus(200); // always 200 so UniOne stops retrying
});

You register that endpoint once, either in the UniOne dashboard or with the webhook/set.json API method, and from then on, every send reports back. The statuses that matter most for login email are delivered, soft_bounced, hard_bounced, and spam; a hard bounce is your cue to nudge the user toward a different email or login path.

Magic Link Authentication - Webhook Setup

Not every project has a backend you can add API calls to. If your page lives inside WordPress, a CMS, or an off-the-shelf plugin, SMTP is often the easier and faster route. You point the system at UniOne's SMTP server, and your emails start flowing through it.

When SMTP is enough

Reach for SMTP when the magic link logic is handled by software you cannot modify, such as a WordPress login plugin, a CMS membership module, an LMS, or a CRM that already has an SMTP settings panel. In such cases, you're not building the token flow yourself. The plugin does this for you. And you just need a reliable way to get its emails delivered. 

For a brand-new application, though, the Web API from the previous section is better long-term, because of the metadata, idempotence, and tracking you get with it.

SMTP settings and TLS

UniOne exposes a standard SMTP interface, so most tools need only the following host, port, credentials, and encryption settings to complete setup:

  • Host: smtp.eu1.unione.io or smtp.us1.unione.io (use the one matching your account region; confirm it on your dashboard's SMTP page)
  • Port: 465 or 587
  • Encryption: TLS; UniOne accepts only secure connections
  • Username: your user ID or project ID
  • Password: your API key or project API key

Configure those once, and now your platform's system mail (logins, magic links, resets, notifications, etc.) routes through UniOne. Further details can be found on the SMTP Service page.

A magic link email has exactly one job: offer the user a single button. Get rid of everything else. The fewer elements in the message, the better it looks on mobile and the cleaner it looks to spam filters.

A plaintext version should be just the essentials:

Sign in to Your App

Use this link to sign in:
https://yourapp.com/auth/verify?token=...

This link expires in 15 minutes.
If you didn't request it, you can ignore this email.

For the HTML version, the UX rules are pretty straightforward; make the call-to-action button the obvious focal point, state the expiry time plainly so the user knows the link won't wait forever, and include a phishing-awareness line, "if you didn't request this, ignore it", so a stray email doesn't alarm anyone. 

Avoid marketing imagery, footers full of links, and anything that competes with the button.

Security checklist

Most of this was already discussed; here is a checklist you can audit against:

  • Short expiry: 15 minutes is a sensible default.
  • One-time use: consume the token on first click; reject every click after.
  • HTTPS-only links: the token should never travel in plain text.
  • Hash tokens at rest: store the SHA-256 hash, never the raw token.
  • Rate limit requests: cap how often one email can request a link, to blunt abuse.
  • Don't leak account existence: return the same response whether the email has an account or not.
  • Avoid open redirects: never build the post-login destination from an unvalidated URL parameter.
  • Log suspicious activity: repeated failures or requests from odd locations are worth flagging.

Common mistakes

The caveats that actually bite teams in production are:

  • Expiry time too long: A link valid for hours is a link that an attacker has hours to use.
  • No SPF/DKIM/DMARC, or sending from a shared domain: This is the fastest way into the spam folder, and for login email, the spam folder means locked-out users.
  • Mixing auth and marketing traffic: One promotional campaign's complaint can sink the reputation your login emails depend on.
  • Leaving link tracking on: Click tracking rewrites the URL and can make a scanner consume the one-time token before your user clicks.
  • The cross-device trap: A user requests the link on their laptop but opens an email on their phone; if your flow assumes the same browser, the sign-in breaks. Either let the link create a session from wherever it's opened, or offer a short code as a fallback.
  • Dead ends on expired links: An expired link should land on a friendly "request a new one" page, not a raw error.

This demo integration will keep working as login volume grows, because it runs on infrastructure built for high load transactional traffic. 

UniOne can send up to 60 million emails per hour, and you stay flexible on how you connect. Templates keep email copy out of your code. You can keep them in UniOne and reference them by ID, with a substitution engine that handles both simple variables and complex Apache Velocity logic.

Visibility is what makes that volume manageable. UniOne logs every send, delivery, and bounce, each timestamped and tied to the metadata you attached, so your metadata and job_id help track any single login attempt. 

Real-time webhooks push events the moment they happen, so you can nudge the user to another path if you see a hard bounce. Dashboards and CSV export cover reporting and debugging. The Projects feature isolates multiple environments under one account, each with its own API key, so staging never pollutes production.

Conclusions

Magic links take the password out of the login process, removing a whole category of friction and risk. The engineering is simple: a random token, short expiry, single use, and a session, but it only works if the email arrives fast and lands in the inbox, because here the email is the login.

That's the part UniOne handles, and the same integration scales without a rewrite. You get server-side templates, webhooks, and analytics for live delivery insight, metadata, and job_id for tracing any send, and Projects to keep products cleanly separated. Built for transactional volume, the service takes you from a handful of logins a day to thousands per minute.

Use the Web API when you're building a backend and want metadata and traceable job_ids; use SMTP when your CMS owns the flow. Keep the expiry short, tracking off, and tokens hashed, and you have passwordless login that's easier for users and harder to attack than the password mechanism it replaces.

  • Transactional Email Service: Send magic links, login alerts, password resets, and other transactional emails on infrastructure built for high deliverability and speed.
  • Email API for Developers: Integrate sending directly into your backend, SaaS, or mobile app, with templates, metadata, webhooks, and analytics.
  • SMTP Service: Connect WordPress, Drupal, online stores, CRMs, and other CMS platforms over SMTP, no custom API work required.
  • Email Deliverability Services: Protect sender reputation and inbox placement so authentication email reaches users reliably.
  • Transactional Email Templates: Ready-made templates for login links, OTP codes, resets, and other security notifications, editable by drag-and-drop or HTML.

FAQ

A magic link is a one-time-use URL emailed to a user that signs them in when clicked, with no password required. It carries a short-lived cryptographic token that your backend validates before creating a session.

Are magic links more secure than passwords? 

In many ways, yes. There's no password to reuse across sites, phish, or expose in a breach, and the token is single-use and expires quickly. The security shifts to the user's email account, so the link's short lifespan and one-time use are what keep the attack window small. 

Keep in mind, however, that this mechanism is as secure as the user’s mailbox; it’s up to the user to keep their mailbox access safe from malicious actors.

A shorter window is best. 15 minutes is a common default, while most teams stay within 30 minutes. Long enough that a user who checks email in a few minutes isn't inconvenienced, short enough that a leaked link is rarely still usable.

Yes. If your login is handled by WordPress, a CMS, or another SMTP-capable code, you can route those emails through UniOne over SMTP. For a custom backend, the Web API is generally preferable for its metadata, idempotence, and tracking.

Why are my magic link emails landing in spam? 

The reason is usually missing or misconfigured SPF, DKIM, and DMARC records, sending from a shared or unverified domain, or mixing authentication mail with marketing traffic. Send from a verified, dedicated subdomain on transactional infrastructure to avoid most problems.

Can UniOne track magic link email delivery? 

Yes. Real time webhooks and event logs show status data such as delivered, soft bounce, hard bounce, and spam complaint, and each send returns a job_id you can store and trace. For example, if a webhook signals a hard bounce, offer the user another way in.

Both deliver a short-lived credential by email, but the last step differs. With an OTP the user enters a code into your app; with a magic link, clicking the link does the verification. Magic links are smooth; OTP codes work better when the email is opened on a different device.

Related Articles
Blog
For beginners
How to Whitelist an Email Address in Gmail, Outlook, Yahoo, and More
Imagine you’re expecting an important email but never receiving it. A few days later you discover th
Vitalii Piddubnyi
30 january 2024, 12:085 min
Blog
For experts
Send Email with JavaScript: Every Method Developers Should Know
Start sending emails with JavaScript in minutes.
Vitalii Piddubnyi
18 march 2026, 05:4120 min
Blog
For beginners
Email Marketing for Travel Agencies: Complete Guide to Get More Bookings
Learn how email marketing boosts sales in travel industry
Valeriia Klymenko
03 september 2025, 07:1015 min