Site Users API
Site Users are per-site customer accounts with OTP-based (passwordless) authentication. Each site has its own user pool, separate from admin users.
Quick Start
import { PerspectApiV2Client } from 'perspectapi-ts-sdk';
const client = new PerspectApiV2Client({
baseUrl: 'https://api.example.com',
apiKey: 'your-api-key'
});
// Request OTP
await client.siteUsers.requestOtp('my-site', {
email: 'user@example.com'
});
// User receives a 6-digit code via email
// Verify OTP and get JWT
const result = await client.siteUsers.verifyOtp('my-site', {
email: 'user@example.com',
code: '123456'
});
console.log(result.token); // JWT for authenticated requests
console.log(result.id); // "su_abc123"
console.log(result.email); // "user@example.com"
Authentication Flow
1. Request OTP
const otpRequest = await client.siteUsers.requestOtp('my-site', {
email: 'user@example.com',
waitlist: false, // Optional: mark as waitlist signup
metadata: { // Optional: custom data stored on the user
referral: 'friend-link',
plan: 'pro'
}
});
// Response:
// {
// object: "otp_request",
// email: "user@example.com",
// expires_in: 900 // seconds (15 minutes)
// }
If the user doesn't exist, they are created automatically with pending_verification status.
2. Verify OTP
const result = await client.siteUsers.verifyOtp('my-site', {
email: 'user@example.com',
code: '123456'
});
// Response includes the user object + JWT token:
// {
// object: "site_user",
// id: "su_abc123",
// email: "user@example.com",
// email_verified: true,
// first_name: null,
// last_name: null,
// status: "active",
// token: "eyJhbGciOiJIUzI1NiIs..."
// }
// Use the token for authenticated requests
client.setAuth(result.token);
3. Store the Token
// Option A: Memory only (most secure, lost on refresh)
client.setAuth(result.token);
// Option B: Your server's httpOnly cookie (recommended for browser apps)
await fetch('/your-api/set-auth-cookie', {
method: 'POST',
body: JSON.stringify({ token: result.token })
});
User Management (Admin)
List Users
const users = await client.siteUsers.list('my-site', {
limit: 20,
status: 'active'
});
// {
// object: "list",
// data: [{ object: "site_user", id: "su_abc", email: "...", ... }],
// has_more: true,
// url: "/v2/sites/my-site/users"
// }
Auto-Paginate All Users
for await (const user of client.siteUsers.listAutoPaginated('my-site')) {
console.log(`${user.id}: ${user.email} (${user.status})`);
}
Get User by ID
const user = await client.siteUsers.get('my-site', 'su_abc123');
Update User
const updated = await client.siteUsers.update('my-site', 'su_abc123', {
first_name: 'Jane',
last_name: 'Smith',
status: 'active',
metadata: { plan: 'enterprise' }
});
User Object Shape
{
"object": "site_user",
"id": "su_abc123",
"email": "user@example.com",
"email_verified": true,
"first_name": "Jane",
"last_name": "Smith",
"avatar_url": null,
"status": "active",
"waitlist": false,
"metadata": { "plan": "enterprise" },
"created_at": "2025-03-01T00:00:00Z",
"updated_at": "2025-04-01T00:00:00Z",
"last_login_at": "2025-04-01T12:00:00Z"
}
Error Handling
import { PerspectV2Error } from 'perspectapi-ts-sdk';
try {
await client.siteUsers.verifyOtp('my-site', { email, code });
} catch (err) {
if (err instanceof PerspectV2Error) {
switch (err.code) {
case 'invalid_code':
showError('Invalid code. Please try again.');
break;
case 'code_expired':
showError('Code expired. Request a new one.');
break;
case 'too_many_attempts':
showError('Too many attempts. Request a new code.');
break;
case 'account_suspended':
showError('Your account has been suspended.');
break;
}
}
}
Framework Example: React Login
import { PerspectApiV2Client, PerspectV2Error } from 'perspectapi-ts-sdk';
const client = new PerspectApiV2Client({
baseUrl: import.meta.env.VITE_API_URL,
apiKey: import.meta.env.VITE_API_KEY
});
function LoginForm() {
const [step, setStep] = useState<'email' | 'code'>('email');
const [email, setEmail] = useState('');
const [code, setCode] = useState('');
const [error, setError] = useState('');
async function requestOtp() {
try {
await client.siteUsers.requestOtp('my-site', { email });
setStep('code');
} catch (err) {
setError('Failed to send code');
}
}
async function verifyOtp() {
try {
const result = await client.siteUsers.verifyOtp('my-site', { email, code });
client.setAuth(result.token);
// Redirect to dashboard
} catch (err) {
if (err instanceof PerspectV2Error) {
setError(err.message);
}
}
}
if (step === 'email') {
return (
<div>
<input type="email" value={email} onChange={e => setEmail(e.target.value)} />
<button onClick={requestOtp}>Send Code</button>
</div>
);
}
return (
<div>
<p>Enter the 6-digit code sent to {email}</p>
<input value={code} onChange={e => setCode(e.target.value)} maxLength={6} />
<button onClick={verifyOtp}>Verify</button>
{error && <p style={{ color: 'red' }}>{error}</p>}
</div>
);
}