PerspectPerspectDocs
Go to Admin
View as Markdown

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>
  );
}