perspectapi-ts-sdk 6.5.9 → 7.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/README.md +46 -1011
  2. package/dist/chunk-K3T2AFYA.mjs +1393 -0
  3. package/dist/index-CWvUyMt3.d.mts +2224 -0
  4. package/dist/index-CWvUyMt3.d.ts +2224 -0
  5. package/dist/index.d.mts +130 -2221
  6. package/dist/index.d.ts +130 -2221
  7. package/dist/index.js +8 -2
  8. package/dist/index.mjs +13 -1364
  9. package/dist/v2/index.d.mts +1 -0
  10. package/dist/v2/index.d.ts +1 -0
  11. package/dist/v2/index.js +1419 -0
  12. package/dist/v2/index.mjs +40 -0
  13. package/docs/README.md +15 -0
  14. package/docs/v1-deprecated/README.md +9 -0
  15. package/docs/v1-deprecated/examples/README.md +324 -0
  16. package/docs/v1-deprecated/examples/basic-usage.ts +258 -0
  17. package/docs/v1-deprecated/examples/cloudflare-worker.ts +274 -0
  18. package/docs/v1-deprecated/examples/content-query-with-slug-prefix.ts +237 -0
  19. package/docs/v1-deprecated/examples/image-transforms.ts +200 -0
  20. package/docs/v1-deprecated/examples/site-user-checkout.ts +186 -0
  21. package/docs/v1-deprecated/examples/slug-prefix-examples.ts +491 -0
  22. package/docs/v1-deprecated/legacy-docs/caching.md +667 -0
  23. package/docs/v1-deprecated/legacy-docs/contact.md +1396 -0
  24. package/docs/v1-deprecated/legacy-docs/csrf-protection.md +664 -0
  25. package/docs/v1-deprecated/legacy-docs/image-transforms.md +523 -0
  26. package/docs/v1-deprecated/legacy-docs/loaders.md +304 -0
  27. package/docs/v1-deprecated/legacy-docs/newsletter.md +811 -0
  28. package/docs/v1-deprecated/legacy-docs/site-users.md +817 -0
  29. package/docs/v1-deprecated/legacy-notes/CHANGELOG-CHECKOUT.md +143 -0
  30. package/docs/v1-deprecated/legacy-notes/CSRF-CHECKOUT.md +271 -0
  31. package/docs/v1-deprecated/legacy-notes/IMAGE_TRANSFORMS_PORT.md +298 -0
  32. package/docs/v1-deprecated/sdk-readme.md +1076 -0
  33. package/examples/README.md +19 -0
  34. package/examples/basic-v2.ts +37 -0
  35. package/llms.txt +25 -0
  36. package/package.json +18 -7
  37. package/src/client/api-keys-client.ts +4 -0
  38. package/src/client/auth-client.ts +4 -0
  39. package/src/client/base-client.ts +7 -0
  40. package/src/client/bundles-client.ts +4 -0
  41. package/src/client/categories-client.ts +4 -0
  42. package/src/client/checkout-client.ts +4 -0
  43. package/src/client/contact-client.ts +4 -0
  44. package/src/client/content-client.ts +4 -0
  45. package/src/client/newsletter-client.ts +4 -0
  46. package/src/client/newsletter-management-client.ts +4 -0
  47. package/src/client/organizations-client.ts +4 -0
  48. package/src/client/products-client.ts +4 -0
  49. package/src/client/site-users-client.ts +10 -1
  50. package/src/client/sites-client.ts +4 -0
  51. package/src/client/webhooks-client.ts +4 -0
  52. package/src/deprecation.ts +2 -1
  53. package/src/index.ts +2 -1
  54. package/src/loaders.ts +59 -0
  55. package/src/perspect-api-client.ts +2 -2
  56. package/src/v2/client/orders-client.ts +6 -1
  57. package/src/v2/types.ts +3 -0
@@ -0,0 +1,817 @@
1
+ # Site Users Guide
2
+
3
+ > Deprecated v1 material. Do not copy these examples into new code. v1 sunsets
4
+ > on 2026-06-01; use `createPerspectApiV2Client` from
5
+ > `perspectapi-ts-sdk/v2` and `/api/v2`.
6
+
7
+ Site Users are per-site customer accounts with OTP-based authentication. Each site can have its own set of users, separate from admin users.
8
+
9
+ ## Table of Contents
10
+
11
+ - [Overview](#overview)
12
+ - [Authentication Flow](#authentication-flow)
13
+ - [Creating Waitlist Users](#creating-waitlist-users)
14
+ - [Managing User Metadata](#managing-user-metadata)
15
+ - [Managing User Profiles](#managing-user-profiles)
16
+ - [Cross-Domain Authentication](#cross-domain-authentication)
17
+ - [User Data Management](#user-data-management)
18
+ - [Orders and Subscriptions](#orders-and-subscriptions)
19
+ - [Admin Operations](#admin-operations)
20
+
21
+ ## Overview
22
+
23
+ Site Users provide passwordless authentication using One-Time Passwords (OTP) sent via email. Each user belongs to a specific site and can have:
24
+
25
+ - **Basic profile**: first name, last name, avatar, email verification status
26
+ - **Waitlist flag**: Mark users as waitlist signups before service launch
27
+ - **Metadata**: Arbitrary JSON object for custom per-site fields
28
+ - **Profile key-values**: Structured storage for phone, addresses, preferences, etc.
29
+ - **Order history**: Linked checkout sessions and transactions
30
+ - **Subscriptions**: Payment subscriptions across providers (Stripe, etc.)
31
+
32
+ ## Authentication Flow
33
+
34
+ ### 1. Request OTP
35
+
36
+ ```typescript
37
+ import { PerspectApiClient } from 'perspectapi-ts-sdk';
38
+
39
+ const client = new PerspectApiClient({
40
+ baseUrl: 'https://api.yoursite.com'
41
+ });
42
+
43
+ // Request OTP for login/signup
44
+ const response = await client.siteUsers.requestOtp(
45
+ 'mysite',
46
+ { email: 'user@example.com' },
47
+ csrfToken // Required for browser-based requests
48
+ );
49
+
50
+ // User receives email with 6-digit code
51
+ ```
52
+
53
+ ### 2. Verify OTP and Get Token
54
+
55
+ ```typescript
56
+ // Option 1: Manual token management (recommended for production)
57
+ const response = await client.siteUsers.verifyOtp(
58
+ 'mysite',
59
+ {
60
+ email: 'user@example.com',
61
+ code: '123456'
62
+ },
63
+ csrfToken
64
+ );
65
+
66
+ const { token, user } = response.data;
67
+
68
+ // Store token securely:
69
+ // - Option A: httpOnly cookie on YOUR domain (recommended)
70
+ await fetch('/your-api/set-auth-cookie', {
71
+ method: 'POST',
72
+ body: JSON.stringify({ token })
73
+ });
74
+ client.setAuth(token);
75
+
76
+ // - Option B: Memory (lost on refresh, most secure)
77
+ client.setAuth(token);
78
+
79
+ // - Option C: localStorage (vulnerable to XSS, not recommended)
80
+ localStorage.setItem('site_user_token', token);
81
+ client.setAuth(token);
82
+
83
+ // Option 2: Automatic token management (memory only)
84
+ const response = await client.siteUsers.verifyOtpAndSetAuth(
85
+ 'mysite',
86
+ { email: 'user@example.com', code: '123456' },
87
+ csrfToken
88
+ );
89
+ // Token automatically set in SDK, lost on page refresh
90
+ ```
91
+
92
+ ### 3. Logout
93
+
94
+ ```typescript
95
+ await client.siteUsers.logout('mysite');
96
+ // Also clear client-side token storage
97
+ client.setAuth(null);
98
+ ```
99
+
100
+ ## Creating Waitlist Users
101
+
102
+ Mark users as waitlist signups before your service launches. You can optionally include metadata during the initial signup:
103
+
104
+ ```typescript
105
+ // Request OTP with waitlist flag and initial metadata
106
+ const response = await client.siteUsers.requestOtp(
107
+ 'mysite',
108
+ {
109
+ email: 'earlyuser@example.com',
110
+ waitlist: true, // Mark as waitlist signup
111
+ metadata: {
112
+ // Capture context at signup time
113
+ signupSource: 'landing-page',
114
+ referralCode: 'FRIEND123',
115
+ interests: ['product-updates', 'beta-features'],
116
+ utm_campaign: 'launch-2024'
117
+ }
118
+ },
119
+ csrfToken
120
+ );
121
+
122
+ // After verification, user will have waitlist: true and metadata
123
+ const { user } = await client.siteUsers.verifyOtp(
124
+ 'mysite',
125
+ { email: 'earlyuser@example.com', code: '123456' },
126
+ csrfToken
127
+ );
128
+
129
+ console.log(user.waitlist); // true
130
+ console.log(user.metadata); // { signupSource: 'landing-page', ... }
131
+ ```
132
+
133
+ ### Updating Existing Users' Metadata
134
+
135
+ If a user already exists (returning user requesting OTP again), providing metadata will **update** their existing metadata:
136
+
137
+ ```typescript
138
+ // First signup
139
+ await client.siteUsers.requestOtp('mysite', {
140
+ email: 'user@example.com',
141
+ metadata: { signupSource: 'web' }
142
+ }, csrfToken);
143
+
144
+ // Later, same user requests OTP again with new metadata
145
+ await client.siteUsers.requestOtp('mysite', {
146
+ email: 'user@example.com',
147
+ metadata: { lastLoginSource: 'mobile-app' } // This replaces previous metadata
148
+ }, csrfToken);
149
+ ```
150
+
151
+ Query waitlist users (admin only):
152
+
153
+ ```typescript
154
+ // Get all waitlist users for a site
155
+ const users = await client.siteUsers.listUsers(
156
+ 'mysite',
157
+ { limit: 100, offset: 0 }
158
+ );
159
+
160
+ const waitlistUsers = users.data.users.filter(u => u.waitlist);
161
+
162
+ // Analyze waitlist metadata
163
+ waitlistUsers.forEach(user => {
164
+ console.log(`${user.email}: ${user.metadata?.signupSource}`);
165
+ });
166
+ ```
167
+
168
+ ## Managing User Metadata
169
+
170
+ Metadata is an arbitrary JSON object for storing custom per-site fields. The client is responsible for merging - the API does **full replacement**.
171
+
172
+ ### Setting Metadata
173
+
174
+ ```typescript
175
+ // Set metadata (full replacement)
176
+ await client.siteUsers.updateMe(
177
+ 'mysite',
178
+ {
179
+ metadata: {
180
+ preferences: {
181
+ theme: 'dark',
182
+ notifications: true,
183
+ language: 'en'
184
+ },
185
+ tags: ['premium', 'early-adopter'],
186
+ customField: 'value',
187
+ onboardingCompleted: true
188
+ }
189
+ },
190
+ csrfToken
191
+ );
192
+ ```
193
+
194
+ ### Merging Metadata (Client-Side Pattern)
195
+
196
+ ```typescript
197
+ // 1. Get current user data
198
+ const { data } = await client.siteUsers.getMe('mysite');
199
+ const currentMetadata = data.user.metadata || {};
200
+
201
+ // 2. Deep merge with new data (example using lodash or custom logic)
202
+ import { merge } from 'lodash';
203
+
204
+ const updatedMetadata = merge({}, currentMetadata, {
205
+ preferences: {
206
+ theme: 'light' // Update just this field, keep others
207
+ }
208
+ });
209
+
210
+ // 3. Send merged result
211
+ await client.siteUsers.updateMe(
212
+ 'mysite',
213
+ { metadata: updatedMetadata },
214
+ csrfToken
215
+ );
216
+ ```
217
+
218
+ ### Shallow Merge Pattern
219
+
220
+ ```typescript
221
+ // Simple shallow merge for top-level keys
222
+ const { data } = await client.siteUsers.getMe('mysite');
223
+
224
+ await client.siteUsers.updateMe(
225
+ 'mysite',
226
+ {
227
+ metadata: {
228
+ ...data.user.metadata,
229
+ newField: 'value',
230
+ tags: [...(data.user.metadata?.tags || []), 'new-tag']
231
+ }
232
+ },
233
+ csrfToken
234
+ );
235
+ ```
236
+
237
+ ### Clearing Metadata
238
+
239
+ ```typescript
240
+ // Clear all metadata
241
+ await client.siteUsers.updateMe(
242
+ 'mysite',
243
+ { metadata: null },
244
+ csrfToken
245
+ );
246
+ ```
247
+
248
+ ## Managing User Profiles
249
+
250
+ User profiles are a **separate** key-value store for structured data like phone numbers, addresses, and preferences. Unlike metadata, each key is stored as a separate database record.
251
+
252
+ ### Setting Profile Values
253
+
254
+ ```typescript
255
+ // Set a simple string value
256
+ await client.siteUsers.setProfileValue(
257
+ 'mysite',
258
+ 'phone',
259
+ '+1-555-0123',
260
+ csrfToken
261
+ );
262
+
263
+ // Set a complex JSON value (addresses, preferences, etc.)
264
+ await client.siteUsers.setProfileValue(
265
+ 'mysite',
266
+ 'address_shipping',
267
+ JSON.stringify({
268
+ line1: '123 Main St',
269
+ city: 'San Francisco',
270
+ state: 'CA',
271
+ postal_code: '94102',
272
+ country: 'US'
273
+ }),
274
+ csrfToken
275
+ );
276
+
277
+ await client.siteUsers.setProfileValue(
278
+ 'mysite',
279
+ 'address_billing',
280
+ JSON.stringify({
281
+ line1: '456 Oak Ave',
282
+ city: 'Portland',
283
+ state: 'OR',
284
+ postal_code: '97201',
285
+ country: 'US'
286
+ }),
287
+ csrfToken
288
+ );
289
+ ```
290
+
291
+ ### Getting Profile Data
292
+
293
+ ```typescript
294
+ // Get all profile key-values
295
+ const { data } = await client.siteUsers.getProfile('mysite');
296
+
297
+ // Access profile values
298
+ console.log(data.profile.phone?.value); // '+1-555-0123'
299
+ console.log(data.profile.address_shipping?.value); // { line1: '123 Main St', ... }
300
+ console.log(data.profile.address_shipping?.updated_at); // '2024-01-15T12:00:00Z'
301
+
302
+ // Or get user with profile in one request
303
+ const response = await client.siteUsers.getMe('mysite');
304
+ console.log(response.data.user); // Basic user fields
305
+ console.log(response.data.profile); // All profile key-values
306
+ ```
307
+
308
+ ### Deleting Profile Values
309
+
310
+ ```typescript
311
+ // Delete a specific profile key
312
+ await client.siteUsers.deleteProfileValue(
313
+ 'mysite',
314
+ 'phone',
315
+ csrfToken
316
+ );
317
+ ```
318
+
319
+ ### Common Profile Keys
320
+
321
+ Suggested key naming conventions:
322
+
323
+ - `phone` - Primary phone number
324
+ - `whatsapp` - WhatsApp number
325
+ - `address_shipping` - Shipping address (JSON)
326
+ - `address_billing` - Billing address (JSON)
327
+ - `preferences` - User preferences (JSON)
328
+ - `notifications` - Notification settings (JSON)
329
+ - `social_twitter` - Twitter handle
330
+ - `social_linkedin` - LinkedIn URL
331
+
332
+ ## Cross-Domain Authentication
333
+
334
+ Site users support cross-domain authentication using JWT tokens in the Authorization header.
335
+
336
+ ### Same-Domain Setup (Default)
337
+
338
+ If your client app is on the **same domain** as the API:
339
+
340
+ ```typescript
341
+ // The API automatically sets httpOnly cookies
342
+ await client.siteUsers.verifyOtp('mysite', { email, code }, csrfToken);
343
+
344
+ // Cookie is automatically sent with subsequent requests
345
+ await client.siteUsers.getMe('mysite');
346
+ ```
347
+
348
+ ### Cross-Domain Setup (Token-Based)
349
+
350
+ If your client app is on a **different domain** than the API:
351
+
352
+ ```typescript
353
+ // 1. Verify OTP and get token
354
+ const response = await client.siteUsers.verifyOtp(
355
+ 'mysite',
356
+ { email, code },
357
+ csrfToken
358
+ );
359
+
360
+ const { token } = response.data;
361
+
362
+ // 2. Store token on YOUR domain (httpOnly cookie via your API)
363
+ await fetch('https://yourclient.com/api/set-auth', {
364
+ method: 'POST',
365
+ credentials: 'include',
366
+ body: JSON.stringify({ token })
367
+ });
368
+
369
+ // 3. Set token in SDK for Authorization header
370
+ client.setAuth(token);
371
+
372
+ // 4. All subsequent requests include Authorization: Bearer <token>
373
+ await client.siteUsers.getMe('mysite');
374
+ ```
375
+
376
+ ## User Data Management
377
+
378
+ ### Updating User Profile
379
+
380
+ ```typescript
381
+ // Update basic user fields
382
+ await client.siteUsers.updateMe(
383
+ 'mysite',
384
+ {
385
+ first_name: 'John',
386
+ last_name: 'Doe',
387
+ avatar_url: 'https://example.com/avatar.jpg',
388
+ metadata: {
389
+ // ... custom metadata
390
+ }
391
+ },
392
+ csrfToken
393
+ );
394
+ ```
395
+
396
+ ### Getting Current User
397
+
398
+ ```typescript
399
+ const { data } = await client.siteUsers.getMe('mysite');
400
+
401
+ console.log(data.user);
402
+ // {
403
+ // id: 'user_abc123',
404
+ // email: 'user@example.com',
405
+ // first_name: 'John',
406
+ // last_name: 'Doe',
407
+ // avatar_url: 'https://...',
408
+ // status: 'active',
409
+ // email_verified: true,
410
+ // waitlist: false,
411
+ // metadata: { ... },
412
+ // created_at: '2024-01-01T00:00:00Z',
413
+ // last_login_at: '2024-01-15T12:00:00Z'
414
+ // }
415
+
416
+ console.log(data.profile);
417
+ // {
418
+ // phone: { value: '+1-555-0123', updated_at: '...' },
419
+ // address_shipping: { value: { line1: '...', ... }, updated_at: '...' }
420
+ // }
421
+ ```
422
+
423
+ ## Orders and Subscriptions
424
+
425
+ ### Getting Order History
426
+
427
+ ```typescript
428
+ // Get all orders for current user
429
+ const { data } = await client.siteUsers.getOrders('mysite', {
430
+ limit: 50,
431
+ offset: 0
432
+ });
433
+
434
+ console.log(data.orders);
435
+ // [
436
+ // {
437
+ // session_id: 'cs_abc123',
438
+ // order_id: 'ord_xyz789',
439
+ // customer_email: 'user@example.com',
440
+ // amount_total: 4999,
441
+ // currency: 'usd',
442
+ // status: 'complete',
443
+ // payment_status: 'paid',
444
+ // line_items: [...],
445
+ // created_at: '2024-01-15T12:00:00Z'
446
+ // }
447
+ // ]
448
+ ```
449
+
450
+ ### Getting Single Order
451
+
452
+ ```typescript
453
+ // Get order detail by ID or session ID
454
+ const { data } = await client.siteUsers.getOrder('mysite', 'ord_xyz789');
455
+
456
+ console.log(data.order);
457
+ ```
458
+
459
+ ### Managing Subscriptions
460
+
461
+ ```typescript
462
+ // Get all subscriptions
463
+ const { data } = await client.siteUsers.getSubscriptions('mysite', {
464
+ limit: 50,
465
+ offset: 0
466
+ });
467
+
468
+ console.log(data.subscriptions);
469
+ // [
470
+ // {
471
+ // id: 'sub_abc123',
472
+ // provider: 'stripe',
473
+ // provider_subscription_id: 'sub_stripe123',
474
+ // plan_name: 'Pro Plan',
475
+ // plan_id: 'price_123',
476
+ // status: 'active',
477
+ // amount: 2999,
478
+ // currency: 'usd',
479
+ // billing_interval: 'month',
480
+ // current_period_start: '2024-01-01T00:00:00Z',
481
+ // current_period_end: '2024-02-01T00:00:00Z',
482
+ // cancel_at_period_end: false,
483
+ // created_at: '2024-01-01T00:00:00Z'
484
+ // }
485
+ // ]
486
+ ```
487
+
488
+ ### Getting Single Subscription
489
+
490
+ ```typescript
491
+ const { data } = await client.siteUsers.getSubscription('mysite', 'sub_abc123');
492
+
493
+ console.log(data.subscription);
494
+ ```
495
+
496
+ ### Canceling Subscription
497
+
498
+ ```typescript
499
+ // Cancel subscription at period end (default behavior)
500
+ const response = await client.siteUsers.cancelSubscription(
501
+ 'mysite',
502
+ 'sub_abc123',
503
+ csrfToken
504
+ );
505
+
506
+ console.log(response.data);
507
+ // { success: true, message: 'Subscription will be canceled at period end' }
508
+
509
+ // Cancel immediately
510
+ await client.siteUsers.cancelSubscription(
511
+ 'mysite',
512
+ 'sub_abc123',
513
+ csrfToken,
514
+ { mode: 'immediate' }
515
+ );
516
+
517
+ // Cancel at a scheduled timestamp (ISO)
518
+ await client.siteUsers.cancelSubscription(
519
+ 'mysite',
520
+ 'sub_abc123',
521
+ csrfToken,
522
+ { mode: 'scheduled', cancel_at: '2026-06-15T14:00:00Z' }
523
+ );
524
+
525
+ // Cancel after a configured delay
526
+ await client.siteUsers.cancelSubscription(
527
+ 'mysite',
528
+ 'sub_abc123',
529
+ csrfToken,
530
+ { mode: 'scheduled', delay_days: 3, delay_hours: 6 }
531
+ );
532
+ ```
533
+
534
+ ### Getting Newsletter Subscriptions
535
+
536
+ ```typescript
537
+ // Get linked newsletter subscriptions
538
+ const { data } = await client.siteUsers.getNewsletterSubscriptions('mysite');
539
+
540
+ console.log(data.newsletters);
541
+ ```
542
+
543
+ ## Admin Operations
544
+
545
+ These endpoints require API key authentication (not site user JWT).
546
+
547
+ ### Listing All Users
548
+
549
+ ```typescript
550
+ // Initialize client with API key
551
+ const adminClient = new PerspectApiClient({
552
+ baseUrl: 'https://api.yoursite.com',
553
+ apiKey: 'your-api-key'
554
+ });
555
+
556
+ // List all users for a site
557
+ const { data } = await adminClient.siteUsers.listUsers(
558
+ 'mysite',
559
+ {
560
+ limit: 100,
561
+ offset: 0,
562
+ status: 'active' // Optional: filter by status
563
+ }
564
+ );
565
+
566
+ console.log(data.users);
567
+ ```
568
+
569
+ ### Getting User Detail (Admin)
570
+
571
+ ```typescript
572
+ // Get full user details including profile
573
+ const { data } = await adminClient.siteUsers.getUser('mysite', 'user_abc123');
574
+
575
+ console.log(data.user);
576
+ console.log(data.profile);
577
+ ```
578
+
579
+ ### Updating User Status (Admin)
580
+
581
+ ```typescript
582
+ // Suspend a user
583
+ await adminClient.siteUsers.updateUserStatus(
584
+ 'mysite',
585
+ 'user_abc123',
586
+ 'suspended',
587
+ csrfToken
588
+ );
589
+
590
+ // Reactivate a user
591
+ await adminClient.siteUsers.updateUserStatus(
592
+ 'mysite',
593
+ 'user_abc123',
594
+ 'active',
595
+ csrfToken
596
+ );
597
+
598
+ // Available statuses: 'active' | 'suspended' | 'pending_verification'
599
+ ```
600
+
601
+ ## Complete Example: User Registration Flow
602
+
603
+ ```typescript
604
+ import { PerspectApiClient } from 'perspectapi-ts-sdk';
605
+
606
+ const client = new PerspectApiClient({
607
+ baseUrl: 'https://api.yoursite.com'
608
+ });
609
+
610
+ // 1. Request OTP with waitlist flag and initial metadata
611
+ async function startSignup(
612
+ email: string,
613
+ isWaitlist: boolean,
614
+ signupContext?: {
615
+ source?: string;
616
+ referralCode?: string;
617
+ utmParams?: Record<string, string>;
618
+ }
619
+ ) {
620
+ const csrfToken = await getCsrfToken(); // Get from your CSRF endpoint
621
+
622
+ const response = await client.siteUsers.requestOtp(
623
+ 'mysite',
624
+ {
625
+ email,
626
+ waitlist: isWaitlist,
627
+ metadata: {
628
+ signupSource: signupContext?.source || 'direct',
629
+ referralCode: signupContext?.referralCode,
630
+ utmParams: signupContext?.utmParams,
631
+ signupTimestamp: new Date().toISOString()
632
+ }
633
+ },
634
+ csrfToken
635
+ );
636
+
637
+ return response.data.success;
638
+ }
639
+
640
+ // 2. Verify OTP and complete signup
641
+ async function completeSignup(email: string, code: string) {
642
+ const csrfToken = await getCsrfToken();
643
+
644
+ const response = await client.siteUsers.verifyOtp(
645
+ 'mysite',
646
+ { email, code },
647
+ csrfToken
648
+ );
649
+
650
+ const { token, user } = response.data;
651
+
652
+ // Store token securely (e.g., httpOnly cookie via your API)
653
+ await storeAuthToken(token);
654
+
655
+ // Set auth for SDK
656
+ client.setAuth(token);
657
+
658
+ return user;
659
+ }
660
+
661
+ // 3. Complete user profile
662
+ async function completeProfile(profileData: {
663
+ first_name: string;
664
+ last_name: string;
665
+ phone: string;
666
+ metadata?: any;
667
+ }) {
668
+ const csrfToken = await getCsrfToken();
669
+
670
+ // Update basic profile
671
+ await client.siteUsers.updateMe(
672
+ 'mysite',
673
+ {
674
+ first_name: profileData.first_name,
675
+ last_name: profileData.last_name,
676
+ metadata: {
677
+ onboardingCompleted: true,
678
+ signupSource: 'website',
679
+ ...profileData.metadata
680
+ }
681
+ },
682
+ csrfToken
683
+ );
684
+
685
+ // Set phone in profile key-values
686
+ await client.siteUsers.setProfileValue(
687
+ 'mysite',
688
+ 'phone',
689
+ profileData.phone,
690
+ csrfToken
691
+ );
692
+
693
+ // Get updated user data
694
+ const { data } = await client.siteUsers.getMe('mysite');
695
+ return data;
696
+ }
697
+
698
+ // Usage
699
+ async function registerUser() {
700
+ // Step 1: Request OTP with signup context
701
+ await startSignup('user@example.com', false, {
702
+ source: 'landing-page',
703
+ referralCode: 'FRIEND123',
704
+ utmParams: {
705
+ utm_source: 'google',
706
+ utm_campaign: 'spring-2024'
707
+ }
708
+ });
709
+ console.log('OTP sent to email');
710
+
711
+ // Step 2: User enters code from email
712
+ const user = await completeSignup('user@example.com', '123456');
713
+ console.log('Signup successful:', user);
714
+ console.log('Signup metadata:', user.metadata);
715
+
716
+ // Step 3: Complete profile
717
+ const userData = await completeProfile({
718
+ first_name: 'John',
719
+ last_name: 'Doe',
720
+ phone: '+1-555-0123',
721
+ metadata: {
722
+ referralSource: 'friend',
723
+ interests: ['tech', 'design']
724
+ }
725
+ });
726
+
727
+ console.log('Profile complete:', userData);
728
+ }
729
+ ```
730
+
731
+ ## Best Practices
732
+
733
+ ### Security
734
+
735
+ 1. **Always use CSRF tokens** for browser-based requests
736
+ 2. **Store tokens securely**:
737
+ - Production: httpOnly cookies on your domain
738
+ - Development: Memory (lost on refresh)
739
+ - Avoid: localStorage (vulnerable to XSS)
740
+ 3. **Validate email addresses** before requesting OTP
741
+ 4. **Rate limit OTP requests** on your frontend
742
+
743
+ ### Data Organization
744
+
745
+ 1. **Use metadata for**:
746
+ - Unstructured custom fields
747
+ - Site-specific configuration
748
+ - Temporary flags or states
749
+ - Analytics data
750
+
751
+ 2. **Use profile key-values for**:
752
+ - Structured data (addresses, phone numbers)
753
+ - Data that needs individual timestamps
754
+ - Data that may be queried separately
755
+ - User preferences with granular updates
756
+
757
+ ### Performance
758
+
759
+ 1. **Batch profile updates** when possible
760
+ 2. **Cache user data** client-side (with TTL)
761
+ 3. **Use pagination** for order/subscription lists
762
+ 4. **Merge metadata client-side** to avoid fetch-update races
763
+
764
+ ## TypeScript Types
765
+
766
+ ```typescript
767
+ interface SiteUser {
768
+ id: string;
769
+ email: string;
770
+ first_name?: string;
771
+ last_name?: string;
772
+ avatar_url?: string;
773
+ status: 'active' | 'suspended' | 'pending_verification';
774
+ email_verified: boolean;
775
+ waitlist: boolean;
776
+ metadata?: Record<string, any>;
777
+ created_at: string;
778
+ last_login_at?: string;
779
+ }
780
+
781
+ interface SiteUserProfile {
782
+ [key: string]: {
783
+ value: any;
784
+ updated_at: string;
785
+ };
786
+ }
787
+
788
+ interface RequestOtpRequest {
789
+ email: string;
790
+ waitlist?: boolean;
791
+ metadata?: Record<string, any>; // Optional metadata to set on user creation/update
792
+ }
793
+
794
+ interface VerifyOtpRequest {
795
+ email: string;
796
+ code: string;
797
+ }
798
+
799
+ interface VerifyOtpResponse {
800
+ success: boolean;
801
+ token: string;
802
+ user: {
803
+ id: string;
804
+ email: string;
805
+ first_name?: string;
806
+ last_name?: string;
807
+ avatar_url?: string;
808
+ };
809
+ }
810
+
811
+ interface UpdateSiteUserRequest {
812
+ first_name?: string;
813
+ last_name?: string;
814
+ avatar_url?: string;
815
+ metadata?: Record<string, any>;
816
+ }
817
+ ```