Skip to main content
Integrate Unosend SMS API with your authentication system to send verification codes, enable 2FA, and implement passwordless login.

Quick Start

All auth integrations follow the same pattern:
  1. Generate a verification code
  2. Store it securely with expiry
  3. Send via Unosend SMS API
  4. Verify the code on callback

NextAuth.js

Implement SMS OTP authentication with NextAuth.js.
// pages/api/auth/[...nextauth].ts
import NextAuth from 'next-auth'
import CredentialsProvider from 'next-auth/providers/credentials'

export default NextAuth({
  providers: [
    CredentialsProvider({
      id: 'sms-otp',
      name: 'SMS OTP',
      credentials: {
        phone: { label: "Phone", type: "text" },
        code: { label: "Code", type: "text" }
      },
      async authorize(credentials) {
        // Verify OTP from your database
        const user = await verifyOTP(credentials.phone, credentials.code)
        return user || null
      }
    })
  ]
})

// Send OTP function
async function sendOTP(phone) {
  const otp = Math.floor(100000 + Math.random() * 900000)
  
  // Store OTP in database with 5-minute expiry
  await db.otps.create({ 
    phone, 
    code: otp, 
    expires: new Date(Date.now() + 300000) 
  })
  
  // Send via Unosend
  await fetch('https://www.unosend.co/api/v1/sms', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${process.env.UNOSEND_API_KEY}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      to: phone,
      body: `Your login code is ${otp}. Valid for 5 minutes.`,
      metadata: { type: 'login' }
    })
  })
}

Clerk

Add SMS verification to Clerk authentication.
// app/api/clerk-sms/route.ts
import { clerkClient } from '@clerk/nextjs/server'

export async function POST(req) {
  const { userId, phone } = await req.json()
  
  // Generate OTP
  const otp = Math.floor(100000 + Math.random() * 900000)
  
  // Store in Clerk user metadata
  await clerkClient.users.updateUserMetadata(userId, {
    privateMetadata: {
      smsOtp: otp,
      smsOtpExpiry: Date.now() + 300000
    }
  })
  
  // Send via Unosend
  const response = await fetch('https://www.unosend.co/api/v1/sms', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${process.env.UNOSEND_API_KEY}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      to: phone,
      body: `Your Clerk verification code is ${otp}`,
      metadata: { userId }
    })
  })
  
  const data = await response.json()
  return Response.json({ success: true, messageId: data.data[0].id })
}

// Verify endpoint
export async function PATCH(req) {
  const { userId, code } = await req.json()
  
  const user = await clerkClient.users.getUser(userId)
  const storedOtp = user.privateMetadata.smsOtp
  const expiry = user.privateMetadata.smsOtpExpiry
  
  if (!storedOtp || Date.now() > expiry) {
    return Response.json({ error: 'Code expired' }, { status: 400 })
  }
  
  if (storedOtp !== parseInt(code)) {
    return Response.json({ error: 'Invalid code' }, { status: 400 })
  }
  
  // Clear OTP after successful verification
  await clerkClient.users.updateUserMetadata(userId, {
    privateMetadata: { smsOtp: null, smsOtpExpiry: null }
  })
  
  return Response.json({ success: true })
}

Supabase Auth

Configure custom SMS provider for Supabase authentication.
// supabase/functions/send-sms/index.ts
Deno.serve(async (req) => {
  const { phone, token, type } = await req.json()
  
  // Customize message based on type
  const messages = {
    signup: `Welcome! Your verification code is ${token}`,
    login: `Your login code is ${token}. Valid for 60 seconds.`,
    recovery: `Your password reset code is ${token}`,
  }
  
  const response = await fetch('https://www.unosend.co/api/v1/sms', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${Deno.env.get('UNOSEND_API_KEY')}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      to: phone,
      body: messages[type] || `Your verification code is ${token}`,
      metadata: { type, timestamp: Date.now() }
    })
  })
  
  const data = await response.json()
  
  return new Response(JSON.stringify(data), {
    headers: { 'Content-Type': 'application/json' }
  })
})
Configure in Supabase Dashboard:
  1. Go to Authentication → Phone
  2. Enable phone auth
  3. Select Custom SMS Provider
  4. Set Hook URL: https://your-project.supabase.co/functions/v1/send-sms
  5. Add UNOSEND_API_KEY to Function Secrets

Firebase Auth

Send SMS verification codes with Firebase Cloud Functions.
// functions/index.js
const functions = require('firebase-functions')
const admin = require('firebase-admin')
admin.initializeApp()

exports.sendSMSVerification = functions.https.onCall(async (data, context) => {
  // Verify user is authenticated
  if (!context.auth) {
    throw new functions.https.HttpsError('unauthenticated', 'Must be logged in')
  }
  
  const { phone } = data
  const uid = context.auth.uid
  
  // Generate 6-digit code
  const code = Math.floor(100000 + Math.random() * 900000)
  
  // Store in Firestore
  await admin.firestore().collection('smsVerifications').doc(uid).set({
    code: code.toString(),
    phone,
    createdAt: admin.firestore.FieldValue.serverTimestamp(),
    expiresAt: new Date(Date.now() + 300000)
  })
  
  // Send via Unosend
  const response = await fetch('https://www.unosend.co/api/v1/sms', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${functions.config().unosend.key}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      to: phone,
      body: `Your verification code is ${code}. Valid for 5 minutes.`,
      metadata: { uid }
    })
  })
  
  const result = await response.json()
  return { success: true, messageId: result.data[0].id }
})

// Verify code
exports.verifySMSCode = functions.https.onCall(async (data, context) => {
  if (!context.auth) {
    throw new functions.https.HttpsError('unauthenticated', 'Must be logged in')
  }
  
  const { code } = data
  const uid = context.auth.uid
  
  const doc = await admin.firestore().collection('smsVerifications').doc(uid).get()
  
  if (!doc.exists) {
    throw new functions.https.HttpsError('not-found', 'No verification code found')
  }
  
  const data = doc.data()
  
  if (new Date() > data.expiresAt.toDate()) {
    throw new functions.https.HttpsError('failed-precondition', 'Code expired')
  }
  
  if (data.code !== code.toString()) {
    throw new functions.https.HttpsError('invalid-argument', 'Invalid code')
  }
  
  // Mark phone as verified
  await admin.auth().updateUser(uid, {
    phoneNumber: data.phone
  })
  
  // Delete verification doc
  await doc.ref.delete()
  
  return { success: true, verified: true }
})
Client usage:
import { getFunctions, httpsCallable } from 'firebase/functions'

const functions = getFunctions()

// Send code
const sendSMS = httpsCallable(functions, 'sendSMSVerification')
const result = await sendSMS({ phone: '+14155551234' })

// Verify code
const verifyCode = httpsCallable(functions, 'verifySMSCode')
await verifyCode({ code: '123456' })
Set config:
firebase functions:config:set unosend.key="un_your_api_key"

Auth0

Add SMS OTP to Auth0 login flow with Actions.
// Auth0 Action: Send SMS OTP
exports.onExecutePostLogin = async (event, api) => {
  // Check if SMS 2FA is enabled for user
  if (event.user.app_metadata.sms_2fa_enabled) {
    const otp = Math.floor(100000 + Math.random() * 900000)
    
    // Store OTP in user metadata
    api.user.setAppMetadata('sms_otp', otp)
    api.user.setAppMetadata('sms_otp_expiry', Date.now() + 300000)
    
    try {
      // Send via Unosend
      const response = await fetch('https://www.unosend.co/api/v1/sms', {
        method: 'POST',
        headers: {
          'Authorization': `Bearer ${event.secrets.UNOSEND_API_KEY}`,
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({
          to: event.user.phone_number,
          body: `Your Auth0 security code is ${otp}. Do not share.`,
          metadata: { userId: event.user.user_id }
        })
      })
      
      if (!response.ok) {
        throw new Error('Failed to send SMS')
      }
      
      // Require OTP verification
      api.multifactor.enable('sms', { message: 'Please verify with SMS code' })
      
    } catch (error) {
      console.error('SMS Error:', error)
      api.access.deny('Failed to send verification code')
    }
  }
}
Add Secret in Auth0:
  1. Go to Actions → Library → Custom
  2. Create action with above code
  3. Add Secret: UNOSEND_API_KEY with your API key
  4. Deploy and add to Login flow

Custom 2FA Implementation

Build your own 2FA system with Unosend.
// app/api/auth/2fa/send/route.ts
import { db } from '@/lib/db'
import { getCurrentUser } from '@/lib/auth'

export async function POST(req: Request) {
  const user = await getCurrentUser()
  
  if (!user?.phone) {
    return Response.json({ error: 'No phone number' }, { status: 400 })
  }
  
  // Generate 6-digit code
  const code = Math.floor(100000 + Math.random() * 900000).toString()
  
  // Store in database with 5-minute expiry
  await db.twoFactorToken.upsert({
    where: { userId: user.id },
    update: {
      code,
      expiresAt: new Date(Date.now() + 5 * 60 * 1000),
      attempts: 0
    },
    create: {
      userId: user.id,
      code,
      expiresAt: new Date(Date.now() + 5 * 60 * 1000)
    }
  })
  
  // Send via Unosend
  const response = await fetch('https://www.unosend.co/api/v1/sms', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${process.env.UNOSEND_API_KEY}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      to: user.phone,
      body: `Your 2FA code is ${code}. Don't share with anyone. Valid for 5 minutes.`,
      metadata: { userId: user.id, type: '2fa' }
    })
  })
  
  if (!response.ok) {
    return Response.json({ error: 'Failed to send SMS' }, { status: 500 })
  }
  
  return Response.json({ success: true })
}

// app/api/auth/2fa/verify/route.ts
export async function POST(req: Request) {
  const user = await getCurrentUser()
  const { code } = await req.json()
  
  const token = await db.twoFactorToken.findUnique({
    where: { userId: user.id }
  })
  
  if (!token) {
    return Response.json({ error: 'No code found' }, { status: 404 })
  }
  
  // Check expiry
  if (new Date() > token.expiresAt) {
    await db.twoFactorToken.delete({ where: { userId: user.id } })
    return Response.json({ error: 'Code expired' }, { status: 400 })
  }
  
  // Check attempts (max 3)
  if (token.attempts >= 3) {
    await db.twoFactorToken.delete({ where: { userId: user.id } })
    return Response.json({ error: 'Too many attempts' }, { status: 429 })
  }
  
  // Verify code
  if (token.code !== code) {
    await db.twoFactorToken.update({
      where: { userId: user.id },
      data: { attempts: token.attempts + 1 }
    })
    return Response.json({ error: 'Invalid code' }, { status: 400 })
  }
  
  // Success - delete token
  await db.twoFactorToken.delete({ where: { userId: user.id } })
  
  // Mark session as 2FA verified
  await updateSession({ twoFactorVerified: true })
  
  return Response.json({ success: true })
}
Replace email magic links with SMS codes.
// app/api/auth/magic-sms/send/route.ts
export async function POST(req: Request) {
  const { phone } = await req.json()
  
  // Generate secure token
  const token = crypto.randomBytes(32).toString('hex')
  const code = Math.floor(100000 + Math.random() * 900000).toString()
  
  // Store in database with 10-minute expiry
  await db.magicLink.create({
    data: {
      phone,
      token,
      code,
      expiresAt: new Date(Date.now() + 10 * 60 * 1000)
    }
  })
  
  // Send via Unosend
  await fetch('https://www.unosend.co/api/v1/sms', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${process.env.UNOSEND_API_KEY}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      to: phone,
      body: `Your login code: ${code}\nOr click: ${process.env.APP_URL}/auth/verify/${token}`,
      metadata: { type: 'magic-link' }
    })
  })
  
  return Response.json({ success: true })
}

// app/api/auth/magic-sms/verify/route.ts
export async function POST(req: Request) {
  const { code, phone } = await req.json()
  
  const link = await db.magicLink.findFirst({
    where: {
      phone,
      code,
      expiresAt: { gt: new Date() }
    }
  })
  
  if (!link) {
    return Response.json({ error: 'Invalid or expired code' }, { status: 400 })
  }
  
  // Create/login user
  let user = await db.user.findUnique({ where: { phone } })
  
  if (!user) {
    user = await db.user.create({ data: { phone } })
  }
  
  // Create session
  await createSession(user.id)
  
  // Delete used link
  await db.magicLink.delete({ where: { id: link.id } })
  
  return Response.json({ success: true, userId: user.id })
}

Common Use Cases

Phone Number Verification

// Verify new phone numbers
async function verifyPhoneNumber(userId, phone) {
  const code = Math.floor(100000 + Math.random() * 900000)
  
  await db.phoneVerification.create({
    userId,
    phone,
    code: code.toString(),
    expiresAt: new Date(Date.now() + 10 * 60 * 1000)
  })
  
  await fetch('https://www.unosend.co/api/v1/sms', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${process.env.UNOSEND_API_KEY}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      to: phone,
      body: `Verify your phone number with code: ${code}`,
      tags: ['verification']
    })
  })
}

Password Reset

// Send password reset code
async function sendPasswordResetSMS(phone) {
  const code = Math.floor(100000 + Math.random() * 900000)
  
  await db.passwordReset.create({
    phone,
    code: code.toString(),
    expiresAt: new Date(Date.now() + 15 * 60 * 1000)
  })
  
  await fetch('https://www.unosend.co/api/v1/sms', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${process.env.UNOSEND_API_KEY}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      to: phone,
      body: `Your password reset code is ${code}. Valid for 15 minutes.`,
      tags: ['password-reset']
    })
  })
}

Security Alerts

// Alert on suspicious activity
async function sendSecurityAlert(user, activity) {
  await fetch('https://www.unosend.co/api/v1/sms', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${process.env.UNOSEND_API_KEY}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      to: user.phone,
      body: `Security alert: ${activity}. If this wasn't you, secure your account immediately.`,
      tags: ['security']
    })
  })
}

Best Practices

Rate Limiting

Prevent abuse with rate limits:
const RATE_LIMITS = {
  PER_PHONE_PER_HOUR: 3,
  PER_PHONE_PER_DAY: 10,
}

async function checkRateLimit(phone) {
  const hourAgo = new Date(Date.now() - 60 * 60 * 1000)
  const dayAgo = new Date(Date.now() - 24 * 60 * 60 * 1000)
  
  const [hourCount, dayCount] = await Promise.all([
    db.smsLog.count({
      where: { phone, createdAt: { gte: hourAgo } }
    }),
    db.smsLog.count({
      where: { phone, createdAt: { gte: dayAgo } }
    })
  ])
  
  if (hourCount >= RATE_LIMITS.PER_PHONE_PER_HOUR) {
    throw new Error('Too many SMS requests. Try again later.')
  }
  
  if (dayCount >= RATE_LIMITS.PER_PHONE_PER_DAY) {
    throw new Error('Daily SMS limit reached.')
  }
}

Secure Storage

Hash codes before storing:
import { createHash } from 'crypto'

function hashCode(code) {
  return createHash('sha256').update(code).digest('hex')
}

// Store hashed
await db.otp.create({
  userId,
  codeHash: hashCode(code),
  expiresAt: new Date(Date.now() + 300000)
})

// Verify
const stored = await db.otp.findUnique({ where: { userId } })
if (hashCode(inputCode) === stored.codeHash) {
  // Valid
}

Code Expiry

Always set expiry times:
  • OTP/2FA: 5 minutes
  • Magic Links: 10 minutes
  • Password Reset: 15 minutes
  • Phone Verification: 10 minutes

Retry Logic

Allow code resend with cooldown:
async function canResend(userId) {
  const lastSent = await db.smsLog.findFirst({
    where: { userId },
    orderBy: { createdAt: 'desc' }
  })
  
  if (!lastSent) return true
  
  const COOLDOWN = 60 * 1000 // 60 seconds
  return Date.now() - lastSent.createdAt.getTime() > COOLDOWN
}

Error Handling

Handle SMS failures gracefully:
try {
  const response = await fetch('https://www.unosend.co/api/v1/sms', { ... })
  
  if (!response.ok) {
    const error = await response.json()
    
    // Handle specific errors
    if (response.status === 429) {
      throw new Error('Rate limit exceeded. Try again later.')
    }
    if (response.status === 402) {
      throw new Error('SMS quota exceeded. Upgrade plan.')
    }
    
    throw new Error(error.message || 'Failed to send SMS')
  }
} catch (error) {
  // Log error
  console.error('SMS Error:', error)
  
  // Fallback to email
  await sendEmailFallback(user.email, code)
}

Environment Variables

# .env
UNOSEND_API_KEY=un_your_api_key
UNOSEND_API_URL=https://www.unosend.co/api/v1

# Rate limits
SMS_RATE_LIMIT_HOUR=3
SMS_RATE_LIMIT_DAY=10

# Code expiry (minutes)
OTP_EXPIRY_MINUTES=5
MAGIC_LINK_EXPIRY_MINUTES=10
PASSWORD_RESET_EXPIRY_MINUTES=15

Resources