How to Send Magic Link, OTP & Verification Emails

Send email verification, magic links, and one-time passcodes with Notify — you own the tokens, we deliver the message.

Auth emails are the same job: generate a short-lived secret in your app, put it in a message, send it. Notify delivers the email. You verify the token when the user returns.

This guide covers email verification, magic links, and OTP / one-time codes with the same Notify send call.

Prerequisites

  • API key from Credentials
  • A way to store hashed tokens (database or cache) with expiry
  • For production from addresses: a verified domain

Shared pattern

  1. Create a random token or numeric OTP in your backend
  2. Store a hash of it with user_id and expires_at (never store the raw secret long-term if you can avoid it)
  3. Build HTML or plain text that includes the link or code
  4. POST https://notify.cx/api/email/send with to, subject, message
  5. On submit/click, validate the secret against your store, then invalidate it

Notify does not issue tokens or manage sessions — only delivery.

Email verification

async function sendVerificationEmail(user: { email: string; id: string }) {
  const token = crypto.randomUUID();
  await db.emailTokens.create({
    userId: user.id,
    type: 'verify',
    tokenHash: await hash(token),
    expiresAt: new Date(Date.now() + 1000 * 60 * 60 * 24)
  });

  const verifyUrl = `https://yourapp.com/verify-email?token=${token}`;

  await fetch('https://notify.cx/api/email/send', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'x-api-key': process.env.NOTIFY_API_KEY!
    },
    body: JSON.stringify({
      to: user.email,
      subject: 'Verify your email',
      message: `<p>Confirm your email:</p><p><a href="${verifyUrl}">Verify email</a></p><p>This link expires in 24 hours.</p>`,
      from: 'noreply@your-verified-domain.com'
    })
  });
}

Magic link (passwordless sign-in)

Same as verification, but the callback creates a session instead of flipping email_verified.

const magicUrl = `https://yourapp.com/auth/magic?token=${token}`;

await fetch('https://notify.cx/api/email/send', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'x-api-key': process.env.NOTIFY_API_KEY!
  },
  body: JSON.stringify({
    to: email,
    subject: 'Your sign-in link',
    message: `<p><a href="${magicUrl}">Sign in to Your App</a></p><p>Expires in 15 minutes. If you did not request this, ignore this email.</p>`,
    from: 'noreply@your-verified-domain.com'
  })
});

Use short TTLs (5–15 minutes). Single-use tokens only.

OTP / one-time code

Prefer 6-digit codes for mobile-friendly flows. Hash before storage.

const code = String(Math.floor(100000 + Math.random() * 900000));

await fetch('https://notify.cx/api/email/send', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'x-api-key': process.env.NOTIFY_API_KEY!
  },
  body: JSON.stringify({
    to: email,
    subject: `${code} is your verification code`,
    message: `<p>Your code is <strong style="font-size:24px;letter-spacing:4px">${code}</strong></p><p>It expires in 10 minutes.</p>`,
    from: 'noreply@your-verified-domain.com'
  })
});

Putting the code in the subject helps users who only glance at the inbox list.

Deliverability tips

  • Send from a verified domain with aligned SPF/DKIM
  • Keep copy short; avoid marketing language in auth mail
  • Include why the email arrived (“you signed up” / “you requested a sign-in”)
  • Rate-limit requests per email to slow abuse
  • Watch logs and webhooks for bounces

Where to next?