What You'll Build
By the end of this tutorial, you'll have a working passwordless authentication system where users sign in with their phone number. The flow is simple:
- User enters their phone number
- Auth1 sends them a 6-digit OTP via SMS
- User enters the code
- Your app receives a JWT token and user ID
- User is authenticated
No passwords stored. No password reset emails. No credential stuffing attacks. The user's phone is their identity. Auth1 handles SMS delivery, VOIP blocking, rate limiting, and fraud detection automatically.
Step 1: Sign Up and Get an API Key
Create your Auth1 account
Go to auth1.ai/signup and create a free account. You'll get an API key that looks like this:
auth1_pk_yourapp_a1b2c3d4e5f6g7h8i9j0...
The free tier gives you 1,000 verifications per month. No credit card required.
Step 2: Install the SDK
Add Auth1 to your project
npm install @auth1/sdk
Or use the REST API directly — no SDK required. Every endpoint is a standard HTTP POST with JSON. The SDK is a thin wrapper for convenience.
Step 3: Backend (Express)
Add auth routes to your Express app
Create two routes: one to request an OTP and one to verify it. This is the complete backend code:
const express = require('express'); const { Auth1Client } = require('@auth1/sdk'); const cookieParser = require('cookie-parser'); const app = express(); app.use(express.json()); app.use(cookieParser()); // Initialize Auth1 client const auth1 = new Auth1Client({ apiKey: process.env.AUTH1_API_KEY, baseUrl: 'https://auth-api.z101.ai' }); // ---- Route 1: Request OTP ---- app.post('/api/auth/request', async (req, res) => { try { const { phone } = req.body; const result = await auth1.requestOTP({ phone }); res.json({ success: true, message: 'Verification code sent' }); } catch (err) { res.status(err.status || 500).json({ error: err.message }); } }); // ---- Route 2: Verify OTP ---- app.post('/api/auth/verify', async (req, res) => { try { const { phone, code } = req.body; const result = await auth1.verifyOTP({ phone, code }); if (result.verified) { // Set httpOnly cookie with the JWT token res.cookie('auth_token', result.token, { httpOnly: true, secure: process.env.NODE_ENV === 'production', sameSite: 'lax', maxAge: 7 * 24 * 60 * 60 * 1000 // 7 days }); res.json({ verified: true, user_id: result.user_id, is_new_user: result.is_new }); } else { res.status(401).json({ error: 'Invalid code' }); } } catch (err) { res.status(err.status || 500).json({ error: err.message }); } }); // ---- Route 3: Get current user ---- app.get('/api/auth/me', async (req, res) => { const token = req.cookies.auth_token; if (!token) { return res.status(401).json({ error: 'Not authenticated' }); } try { const user = await auth1.getUser(token); res.json(user); } catch (err) { res.status(401).json({ error: 'Invalid token' }); } }); // ---- Route 4: Logout ---- app.post('/api/auth/logout', (req, res) => { res.clearCookie('auth_token'); res.json({ success: true }); }); app.listen(3000, () => { console.log('Server running on port 3000'); });
That's the entire backend. Four routes, ~60 lines. Auth1 handles OTP generation, SMS delivery, rate limiting, VOIP blocking, user creation, and JWT signing.
Step 4: Frontend
Build a simple login form
Here's a minimal React component (works with Next.js, Vite, or Create React App):
import { useState } from 'react'; export default function LoginForm() { const [phone, setPhone] = useState(''); const [code, setCode] = useState(''); const [step, setStep] = useState('phone'); // 'phone' | 'code' | 'done' const [error, setError] = useState(''); const [loading, setLoading] = useState(false); async function requestOTP(e) { e.preventDefault(); setLoading(true); setError(''); const res = await fetch('/api/auth/request', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ phone }) }); const data = await res.json(); setLoading(false); if (res.ok) { setStep('code'); } else { setError(data.error); } } async function verifyOTP(e) { e.preventDefault(); setLoading(true); setError(''); const res = await fetch('/api/auth/verify', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ phone, code }) }); const data = await res.json(); setLoading(false); if (data.verified) { setStep('done'); // Redirect to dashboard or reload page window.location.href = '/dashboard'; } else { setError(data.error || 'Invalid code'); } } if (step === 'done') { return <p>Authenticated! Redirecting...</p>; } return ( <div style={{ maxWidth: 400, margin: '100px auto', padding: 24 }}> <h1>Sign In</h1> {step === 'phone' ? ( <form onSubmit={requestOTP}> <label>Phone Number</label> <input type="tel" placeholder="+1 (555) 123-4567" value={phone} onChange={e => setPhone(e.target.value)} required /> <button type="submit" disabled={loading}> {loading ? 'Sending...' : 'Send Code'} </button> </form> ) : ( <form onSubmit={verifyOTP}> <p>Enter the 6-digit code sent to {phone}</p> <input type="text" placeholder="000000" maxLength={6} value={code} onChange={e => setCode(e.target.value)} autoFocus required /> <button type="submit" disabled={loading}> {loading ? 'Verifying...' : 'Verify'} </button> <button type="button" onClick={() => setStep('phone')}> Back </button> </form> )} {error && <p style={{ color: 'red' }}>{error}</p>} </div> ); }
Step 5: Deploy
Set the environment variable and ship
AUTH1_API_KEY=auth1_pk_yourapp_a1b2c3d4e5f6g7h8i9j0...
That's it. Deploy to Vercel, Railway, Render, or anywhere that runs Node.js.
The AUTH1_API_KEY environment variable is the only configuration needed.
# Set the environment variable vercel env add AUTH1_API_KEY # Deploy vercel --prod
Alternative: Next.js App Router
If you're using Next.js 14+ with the App Router, here's the server-side implementation using Server Actions:
'use server'; import { cookies } from 'next/headers'; const AUTH1_URL = 'https://auth-api.z101.ai'; const API_KEY = process.env.AUTH1_API_KEY!; export async function requestOTP(phone: string) { const res = await fetch(`${AUTH1_URL}/api/auth/request`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'x-api-key': API_KEY }, body: JSON.stringify({ phone }) }); if (!res.ok) { const err = await res.json(); throw new Error(err.message || 'Failed to send OTP'); } return { success: true }; } export async function verifyOTP(phone: string, code: string) { const res = await fetch(`${AUTH1_URL}/api/auth/verify`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'x-api-key': API_KEY }, body: JSON.stringify({ phone, code }) }); const data = await res.json(); if (data.verified) { // Set httpOnly cookie cookies().set('auth_token', data.token, { httpOnly: true, secure: true, sameSite: 'lax', maxAge: 60 * 60 * 24 * 7 }); } return data; }
Without the SDK (Raw HTTP)
Don't want to install the SDK? The API is two HTTP calls:
# 1. Request OTP curl -X POST https://auth-api.z101.ai/api/auth/request \ -H "Content-Type: application/json" \ -H "x-api-key: auth1_pk_yourapp_..." \ -d '{"phone": "+15551234567"}' # Response: {"success": true, "message": "OTP sent"} # 2. Verify OTP curl -X POST https://auth-api.z101.ai/api/auth/verify \ -H "Content-Type: application/json" \ -H "x-api-key: auth1_pk_yourapp_..." \ -d '{"phone": "+15551234567", "code": "482913"}' # Response: # { # "verified": true, # "token": "eyJhbGciOiJIUzI1NiIs...", # "user_id": "usr_abc123", # "is_new": true # }
That's it. Two endpoints. Works from any language, any framework. Python, Go, Ruby, PHP — if it can make HTTP requests, it can use Auth1.
Every request through Auth1 automatically gets: VOIP phone blocking (no Google Voice signups), rate limiting (5 OTPs per hour per phone), 6-digit secure random OTP generation, 10-minute expiry, 5 attempt maximum, constant-time comparison, and one-time use (replay prevention). You don't configure any of this — it's on by default.