perspectapi-ts-sdk 6.5.7 → 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 (58) 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 -2201
  6. package/dist/index.d.ts +130 -2201
  7. package/dist/index.js +15 -2
  8. package/dist/index.mjs +13 -1357
  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/client/subscriptions-client.ts +15 -0
  58. package/src/v2/types.ts +23 -0
@@ -0,0 +1,811 @@
1
+ # Newsletter Subscription API Documentation
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
+ The PerspectAPI SDK provides comprehensive newsletter support with GDPR-compliant double opt-in, list segmentation, preference management, and a strict split between public and management surfaces:
8
+
9
+ - `client.newsletter`: public subscription flows only.
10
+ - `client.newsletterManagement`: authenticated management APIs under `/newsletter/management/*`.
11
+
12
+ ## Table of Contents
13
+
14
+ - [Quick Start](#quick-start)
15
+ - [Core Features](#core-features)
16
+ - [Subscription Flow](#subscription-flow)
17
+ - [API Reference](#api-reference)
18
+ - [Public Methods](#public-methods)
19
+ - [Management Methods](#management-methods)
20
+ - [Best Practices](#best-practices)
21
+ - [Caching and Webhook Invalidation](#caching-and-webhook-invalidation)
22
+ - [Examples](#examples)
23
+
24
+ ## Quick Start
25
+
26
+ ```typescript
27
+ import { createPerspectApiClient } from 'perspectapi-ts-sdk';
28
+
29
+ const client = createPerspectApiClient({
30
+ baseUrl: 'https://api.example.com',
31
+ apiKey: 'your-api-key'
32
+ });
33
+
34
+ // Browser-based subscription with CSRF protection
35
+ const csrfResponse = await client.getCsrfToken('my-site');
36
+ const csrfToken = csrfResponse.data.token;
37
+
38
+ await client.newsletter.subscribe('my-site', {
39
+ email: 'user@example.com',
40
+ name: 'John Doe'
41
+ }, csrfToken); // Pass CSRF token for security
42
+ ```
43
+
44
+ ## Core Features
45
+
46
+ - **Double Opt-in**: GDPR-compliant email confirmation system
47
+ - **One-click Unsubscribe**: Permanent tokens for easy unsubscription
48
+ - **List Management**: Multiple segmented lists per site
49
+ - **Preference Center**: User-controlled frequency, topics, and privacy settings
50
+ - **Engagement Tracking**: Opens, clicks, bounces, and complaints
51
+ - **Rate Limiting**: Built-in abuse prevention
52
+ - **Import/Export**: Bulk operations for migration
53
+ - **Statistics**: Comprehensive analytics and reporting
54
+
55
+ ## Subscription Flow
56
+
57
+ ### 1. Initial Subscription
58
+
59
+ ```typescript
60
+ // For browser-based forms, always use CSRF protection
61
+ const csrfResponse = await client.getCsrfToken('my-site');
62
+ const csrfToken = csrfResponse.data.token;
63
+
64
+ const result = await client.newsletter.subscribe('my-site', {
65
+ email: 'subscriber@example.com',
66
+ name: 'Jane Smith',
67
+ list_ids: ['list_weekly_digest', 'list_product_updates'],
68
+ frequency: 'weekly',
69
+ topics: ['tech', 'business'],
70
+ double_opt_in: true, // Default: true
71
+ turnstile_token: 'cf-turnstile-response', // Optional CAPTCHA
72
+ metadata: {
73
+ source: 'homepage-footer',
74
+ campaign: 'summer-2024'
75
+ }
76
+ }, csrfToken); // Pass CSRF token
77
+
78
+ // Response
79
+ {
80
+ success: true,
81
+ code: "PENDING_CONFIRMATION",
82
+ message: "Please check your email to confirm your subscription",
83
+ status: "pending_confirmation"
84
+ }
85
+ ```
86
+
87
+ ### 2. Email Confirmation
88
+
89
+ User receives email with confirmation link containing a unique token:
90
+
91
+ ```typescript
92
+ // When user clicks confirmation link
93
+ const confirmed = await client.newsletter.confirmSubscription(
94
+ 'my-site',
95
+ 'confirmation-token-from-email'
96
+ );
97
+
98
+ // Response
99
+ {
100
+ success: true,
101
+ code: "SUBSCRIPTION_CONFIRMED",
102
+ message: "Subscription confirmed successfully",
103
+ status: "confirmed",
104
+ subscription: {
105
+ email: "subscriber@example.com",
106
+ name: "Jane Smith",
107
+ frequency: "weekly"
108
+ }
109
+ }
110
+ ```
111
+
112
+ ### 3. Managing Preferences
113
+
114
+ ```typescript
115
+ // Update subscription preferences (requires CSRF in browser)
116
+ const csrfToken = (await client.getCsrfToken('my-site')).data.token;
117
+
118
+ await client.newsletter.updatePreferences('my-site', 'subscriber@example.com', {
119
+ frequency: 'monthly',
120
+ topics: ['product-updates', 'company-news'],
121
+ email_format: 'html',
122
+ timezone: 'Europe/London',
123
+ track_opens: true,
124
+ track_clicks: true
125
+ }, csrfToken);
126
+ ```
127
+
128
+ ### 4. Unsubscription
129
+
130
+ ```typescript
131
+ // Method 1: Unsubscribe by email (requires CSRF in browser)
132
+ const csrfToken = (await client.getCsrfToken('my-site')).data.token;
133
+ await client.newsletter.unsubscribe('my-site', {
134
+ email: 'subscriber@example.com',
135
+ reason: 'Too frequent'
136
+ }, csrfToken);
137
+
138
+ // Method 2: One-click unsubscribe (from email link, no CSRF needed)
139
+ await client.newsletter.unsubscribeByToken('my-site', 'unsubscribe-token');
140
+ ```
141
+
142
+ ## Response Format
143
+
144
+ All newsletter API methods return responses in a standardized format with consistent success/error handling:
145
+
146
+ ### Success Responses
147
+
148
+ All successful responses include:
149
+ - `success: true` - Indicates the operation completed successfully
150
+ - `code: string` - Machine-readable status code for programmatic handling
151
+ - `message: string` - Human-readable message for display to users
152
+ - `status?: string` - Operation-specific status (e.g., "pending_confirmation", "confirmed")
153
+
154
+ ```typescript
155
+ // Successful subscription (pending confirmation)
156
+ {
157
+ success: true,
158
+ code: "PENDING_CONFIRMATION",
159
+ message: "Please check your email to confirm your subscription",
160
+ status: "pending_confirmation"
161
+ }
162
+
163
+ // Already subscribed
164
+ {
165
+ success: true,
166
+ code: "ALREADY_SUBSCRIBED",
167
+ message: "This email is already subscribed to our newsletter",
168
+ status: "confirmed"
169
+ }
170
+
171
+ // Subscription confirmed
172
+ {
173
+ success: true,
174
+ code: "SUBSCRIPTION_CONFIRMED",
175
+ message: "Subscription confirmed successfully",
176
+ status: "confirmed",
177
+ subscription: { /* subscription details */ }
178
+ }
179
+ ```
180
+
181
+ ### Error Responses
182
+
183
+ Failed operations include:
184
+ - `success: false` - Indicates the operation failed
185
+ - `code: string` - Machine-readable error code
186
+ - `error: string` - Error message describing what went wrong
187
+ - `details?: any` - Additional error details (for validation errors)
188
+
189
+ ```typescript
190
+ // Rate limiting
191
+ {
192
+ success: false,
193
+ code: "RATE_LIMITED",
194
+ error: "Too many subscription attempts. Please try again later."
195
+ }
196
+
197
+ // Validation error
198
+ {
199
+ success: false,
200
+ code: "VALIDATION_ERROR",
201
+ error: "Invalid subscription data",
202
+ details: { /* validation error details */ }
203
+ }
204
+
205
+ // Invalid token
206
+ {
207
+ success: false,
208
+ code: "CONFIRMATION_FAILED",
209
+ error: "Invalid or expired confirmation token"
210
+ }
211
+ ```
212
+
213
+ ### Response Codes
214
+
215
+ #### Success Codes (`success: true`)
216
+ - `PENDING_CONFIRMATION` - Confirmation email sent
217
+ - `SUBSCRIBED` - Subscription confirmed immediately
218
+ - `ALREADY_SUBSCRIBED` - Email already subscribed
219
+ - `CONFIRMATION_RESENT` - Confirmation email resent
220
+ - `SUBSCRIPTION_CONFIRMED` - Token confirmation successful
221
+ - `UNSUBSCRIBED` - Successfully unsubscribed
222
+ - `UNSUBSCRIBE_CONFIRMATION_SENT` - Unsubscribe email sent
223
+
224
+ #### Error Codes (`success: false`)
225
+ - `VALIDATION_ERROR` - Invalid input data
226
+ - `RATE_LIMITED` - Too many requests
227
+ - `MISSING_TOKEN` - Required token not provided
228
+ - `CONFIRMATION_FAILED` - Token validation failed
229
+ - `SUBSCRIPTION_NOT_FOUND` - Subscription doesn't exist
230
+ - `UNSUBSCRIBE_FAILED` - Unsubscribe operation failed
231
+ - `SERVER_ERROR` - Internal server error
232
+
233
+ ### Handling Responses
234
+
235
+ ```typescript
236
+ const result = await client.newsletter.subscribe('my-site', {
237
+ email: 'user@example.com',
238
+ name: 'John Doe'
239
+ }, csrfToken);
240
+
241
+ // Type-safe response handling
242
+ if (result.data?.success) {
243
+ switch (result.data.code) {
244
+ case 'PENDING_CONFIRMATION':
245
+ showMessage('Please check your email to confirm subscription');
246
+ break;
247
+ case 'ALREADY_SUBSCRIBED':
248
+ showMessage('You are already subscribed!');
249
+ break;
250
+ case 'SUBSCRIBED':
251
+ showMessage('Welcome! You have been subscribed.');
252
+ break;
253
+ }
254
+ } else {
255
+ switch (result.data?.code) {
256
+ case 'RATE_LIMITED':
257
+ showError('Please slow down and try again later');
258
+ break;
259
+ case 'VALIDATION_ERROR':
260
+ showError('Please check your email address');
261
+ break;
262
+ default:
263
+ showError(result.data?.error || 'Something went wrong');
264
+ }
265
+ }
266
+ ```
267
+
268
+ ## API Reference
269
+
270
+ ### Public Methods
271
+
272
+ #### `subscribe(siteName, data)`
273
+
274
+ Subscribe a new email to the newsletter.
275
+
276
+ ```typescript
277
+ interface CreateNewsletterSubscriptionRequest {
278
+ email: string; // Required
279
+ name?: string; // Subscriber's name
280
+ list_ids?: string[]; // List IDs to subscribe to
281
+ frequency?: 'instant' | 'daily' | 'weekly' | 'monthly';
282
+ topics?: string[]; // Content topics of interest
283
+ language?: string; // Preferred language (default: 'en')
284
+ source?: string; // Source of subscription
285
+ source_url?: string; // URL where subscription occurred
286
+ double_opt_in?: boolean; // Require email confirmation (default: true)
287
+ turnstile_token?: string; // Cloudflare Turnstile token
288
+ metadata?: Record<string, any>; // Custom metadata
289
+ }
290
+ ```
291
+
292
+ #### `confirmSubscription(siteName, token)`
293
+
294
+ Confirm a pending subscription using the token from confirmation email.
295
+
296
+ #### `unsubscribe(siteName, data)`
297
+
298
+ Unsubscribe an email address.
299
+
300
+ ```typescript
301
+ interface NewsletterUnsubscribeRequest {
302
+ token?: string; // Unsubscribe token (for one-click)
303
+ email?: string; // Email address (alternative to token)
304
+ reason?: string; // Optional feedback
305
+ }
306
+ ```
307
+
308
+ #### `updatePreferences(siteName, email, preferences)`
309
+
310
+ Update subscription preferences.
311
+
312
+ ```typescript
313
+ interface NewsletterPreferences {
314
+ frequency?: 'instant' | 'daily' | 'weekly' | 'monthly';
315
+ topics?: string[];
316
+ language?: string;
317
+ email_format?: 'html' | 'text' | 'both';
318
+ timezone?: string;
319
+ track_opens?: boolean;
320
+ track_clicks?: boolean;
321
+ }
322
+ ```
323
+
324
+ #### `getStatus(siteName, email)`
325
+
326
+ Check if an email is subscribed.
327
+
328
+ #### `getLists(siteName)`
329
+
330
+ Get all public newsletter lists available for subscription.
331
+
332
+ #### `getPublishedCampaigns(siteName, params?, cachePolicy?)`
333
+
334
+ Get publicly available newsletter campaigns (published only).
335
+
336
+ ```typescript
337
+ const campaigns = await client.newsletter.getPublishedCampaigns('my-site', {
338
+ page: 1,
339
+ limit: 20,
340
+ search: 'spring launch'
341
+ });
342
+ ```
343
+
344
+ #### `getPublishedCampaignBySlug(siteName, slug, optionsOrPolicy?, cachePolicy?)`
345
+
346
+ Get a single published newsletter campaign by bare slug.
347
+ If your URL uses a prefix segment, pass it via `slugPrefix`.
348
+
349
+ ```typescript
350
+ const campaign = await client.newsletter.getPublishedCampaignBySlug(
351
+ 'my-site',
352
+ 'spring-launch',
353
+ { slugPrefix: 'updates' }
354
+ );
355
+ ```
356
+
357
+ #### `invalidatePublishedCampaignCacheFromWebhook(payload, fallbackSiteName?)`
358
+
359
+ Convert a webhook payload into newsletter cache tags and invalidate matching entries.
360
+
361
+ This helper only invalidates for publish events. Create or draft updates are ignored.
362
+
363
+ ```typescript
364
+ const result = await client.newsletter.invalidatePublishedCampaignCacheFromWebhook(
365
+ webhookPayload,
366
+ 'my-site'
367
+ );
368
+
369
+ if (result.invalidated) {
370
+ console.log(result.tags); // tags that were invalidated
371
+ }
372
+ ```
373
+
374
+ ### Management Methods
375
+
376
+ All management methods are on `client.newsletterManagement`.
377
+
378
+ #### `syncSubscription(siteName, data)`
379
+
380
+ Canonical app-side upsert by email:
381
+
382
+ ```typescript
383
+ const sync = await client.newsletterManagement.syncSubscription('my-site', {
384
+ email: 'user@example.com',
385
+ name: 'User Name',
386
+ list_ids: ['list_default'],
387
+ resubscribe_override: false
388
+ });
389
+
390
+ console.log(sync.data.code);
391
+ // CREATED | UPDATED | RESUBSCRIBED | ALREADY_UNSUBSCRIBED
392
+ ```
393
+
394
+ #### `listSubscriptions(siteName, params)`
395
+
396
+ List and filter subscribers.
397
+
398
+ ```typescript
399
+ const subscriptions = await client.newsletterManagement.listSubscriptions('my-site', {
400
+ page: 1,
401
+ limit: 100,
402
+ status: 'confirmed',
403
+ list_id: 'list_default'
404
+ });
405
+ ```
406
+
407
+ #### `importSubscriptions(siteName, data)`
408
+
409
+ Bulk import with request-level and row-level `resubscribe_override` (row-level wins).
410
+
411
+ ```typescript
412
+ const result = await client.newsletterManagement.importSubscriptions('my-site', {
413
+ resubscribe_override: false,
414
+ rows: [
415
+ { email: 'user1@example.com', list_ids: ['list_default'] },
416
+ { email: 'user2@example.com', resubscribe_override: true }
417
+ ]
418
+ });
419
+
420
+ console.log(result.data.applied, result.data.skipped_unsubscribed);
421
+ ```
422
+
423
+ #### `createList`, `updateList`, `deleteList`
424
+
425
+ Manage segmented lists:
426
+
427
+ ```typescript
428
+ const list = await client.newsletterManagement.createList('my-site', {
429
+ list_name: 'Premium Members',
430
+ slug: 'premium-members',
431
+ is_public: false
432
+ });
433
+ ```
434
+
435
+ #### `createCampaign`, `updateCampaign`, `deleteCampaign`, `sendCampaignTest`
436
+
437
+ Manage campaigns and send permission-gated test sends:
438
+
439
+ ```typescript
440
+ await client.newsletterManagement.createCampaign('my-site', {
441
+ campaign_name: 'Spring Launch',
442
+ subject: 'Spring Launch',
443
+ markdown_content: '# Hello',
444
+ status: 'draft'
445
+ });
446
+ ```
447
+
448
+ #### `createExport(siteName, data)` and `downloadExport(siteName, exportId)`
449
+
450
+ Exports support `csv` and `json`. `xlsx` is explicitly unsupported.
451
+
452
+ ```typescript
453
+ const exportInfo = await client.newsletterManagement.createExport('my-site', {
454
+ format: 'csv'
455
+ });
456
+
457
+ const csv = await client.newsletterManagement.downloadExport(
458
+ 'my-site',
459
+ exportInfo.data.exportId
460
+ );
461
+ ```
462
+
463
+ ## Best Practices
464
+
465
+ ### 1. Always Use Double Opt-in
466
+
467
+ ```typescript
468
+ // Good: Explicit double opt-in
469
+ await client.newsletter.subscribe('my-site', {
470
+ email: 'user@example.com',
471
+ double_opt_in: true // Default, but be explicit
472
+ });
473
+
474
+ // Only skip for imports from verified sources
475
+ await client.newsletterManagement.importSubscriptions('my-site', {
476
+ rows: verifiedUsers,
477
+ resubscribe_override: true // Only for trusted data
478
+ });
479
+ ```
480
+
481
+ ### 2. Handle Rate Limiting
482
+
483
+ The API includes built-in rate limiting. Handle 429 responses gracefully:
484
+
485
+ ```typescript
486
+ try {
487
+ await client.newsletter.subscribe('my-site', data);
488
+ } catch (error) {
489
+ if (error.status === 429) {
490
+ // Too many requests - implement exponential backoff
491
+ await wait(5000);
492
+ // Retry
493
+ }
494
+ }
495
+ ```
496
+
497
+ ### 3. Segment with Lists
498
+
499
+ Create targeted lists for better engagement:
500
+
501
+ ```typescript
502
+ // Create segmented lists
503
+ await client.newsletterManagement.createList('my-site', {
504
+ list_name: 'Tech Enthusiasts',
505
+ slug: 'tech-news',
506
+ description: 'Latest technology updates'
507
+ });
508
+
509
+ await client.newsletterManagement.createList('my-site', {
510
+ list_name: 'Business Updates',
511
+ slug: 'business-news',
512
+ description: 'Business and finance news'
513
+ });
514
+
515
+ // Subscribe to specific lists
516
+ await client.newsletter.subscribe('my-site', {
517
+ email: 'techie@example.com',
518
+ list_ids: ['list_tech_news']
519
+ });
520
+ ```
521
+
522
+ ### 4. Respect User Preferences
523
+
524
+ ```typescript
525
+ // Check preferences before sending
526
+ const status = await client.newsletter.getStatus('my-site', email);
527
+
528
+ if (status.data.subscribed && status.data.frequency === 'instant') {
529
+ // OK to send immediate notification
530
+ } else {
531
+ // Queue for digest based on frequency
532
+ }
533
+ ```
534
+
535
+ ### 5. Monitor Engagement
536
+
537
+ ```typescript
538
+ // Regular monitoring
539
+ const stats = await client.newsletterManagement.getStats('my-site', {
540
+ startDate: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString(),
541
+ endDate: new Date().toISOString()
542
+ });
543
+
544
+ if ((stats.data.byStatus.unsubscribed || 0) > stats.data.total * 0.05) {
545
+ // High unsubscribe rate - investigate
546
+ }
547
+ ```
548
+
549
+ ## Caching and Webhook Invalidation
550
+
551
+ Published newsletter endpoints are cache-aware, including:
552
+
553
+ - `getLists(siteName)`
554
+ - `getPublishedCampaigns(siteName, params?)`
555
+ - `getPublishedCampaignBySlug(siteName, slug, { slugPrefix? })`
556
+
557
+ Recommended webhook flow:
558
+
559
+ 1. Receive and verify webhook payload.
560
+ 2. Call `invalidatePublishedCampaignCacheFromWebhook(payload, fallbackSiteName?)`.
561
+ 3. Only if `result.invalidated === true`, log/use `result.tags`.
562
+
563
+ ```typescript
564
+ const result = await client.newsletter.invalidatePublishedCampaignCacheFromWebhook(
565
+ payload,
566
+ 'my-site'
567
+ );
568
+
569
+ if (result.invalidated) {
570
+ console.log('Invalidated newsletter tags:', result.tags);
571
+ }
572
+ ```
573
+
574
+ The helper intentionally invalidates on publish transitions (for example `newsletter.published` or `content.published` with `origin_type = newsletter_campaign`) and does not invalidate on draft creation.
575
+
576
+ ## Examples
577
+
578
+ ### React Newsletter Signup Form with CSRF Protection
579
+
580
+ ```tsx
581
+ import { useState, useEffect } from 'react';
582
+ import { createPerspectApiClient } from 'perspectapi-ts-sdk';
583
+
584
+ function NewsletterSignup() {
585
+ const [email, setEmail] = useState('');
586
+ const [status, setStatus] = useState('idle');
587
+ const [csrfToken, setCsrfToken] = useState<string | null>(null);
588
+
589
+ const client = createPerspectApiClient({
590
+ baseUrl: process.env.REACT_APP_API_URL,
591
+ apiKey: process.env.REACT_APP_API_KEY
592
+ });
593
+
594
+ // Fetch CSRF token when component mounts
595
+ useEffect(() => {
596
+ async function fetchCsrfToken() {
597
+ try {
598
+ const response = await client.getCsrfToken('my-site');
599
+ setCsrfToken(response.data.token || '');
600
+ } catch (error) {
601
+ console.error('Failed to fetch CSRF token:', error);
602
+ }
603
+ }
604
+ fetchCsrfToken();
605
+ }, []);
606
+
607
+ const handleSubmit = async (e: React.FormEvent) => {
608
+ e.preventDefault();
609
+
610
+ if (!csrfToken) {
611
+ alert('Security token not loaded. Please refresh and try again.');
612
+ return;
613
+ }
614
+
615
+ setStatus('subscribing');
616
+
617
+ try {
618
+ const result = await client.newsletter.subscribe('my-site', {
619
+ email,
620
+ source: 'website-footer',
621
+ source_url: window.location.href
622
+ }, csrfToken); // Pass CSRF token
623
+
624
+ setStatus('success');
625
+ setEmail('');
626
+ alert('Please check your email to confirm subscription!');
627
+ } catch (error) {
628
+ setStatus('error');
629
+ console.error('Subscription failed:', error);
630
+ }
631
+ };
632
+
633
+ return (
634
+ <form onSubmit={handleSubmit}>
635
+ <input
636
+ type="email"
637
+ value={email}
638
+ onChange={(e) => setEmail(e.target.value)}
639
+ placeholder="Enter your email"
640
+ required
641
+ disabled={status === 'subscribing' || !csrfToken}
642
+ />
643
+ <button type="submit" disabled={status === 'subscribing' || !csrfToken}>
644
+ {status === 'subscribing' ? 'Subscribing...' : 'Subscribe'}
645
+ </button>
646
+ {status === 'success' && <p>Check your email to confirm!</p>}
647
+ {status === 'error' && <p>Something went wrong. Please try again.</p>}
648
+ </form>
649
+ );
650
+ }
651
+ ```
652
+
653
+ ### Cloudflare Worker for Newsletter Confirmation
654
+
655
+ ```typescript
656
+ // Handle confirmation links in a Cloudflare Worker
657
+ export default {
658
+ async fetch(request: Request, env: Env): Promise<Response> {
659
+ const url = new URL(request.url);
660
+
661
+ // Match /newsletter/confirm/:token
662
+ const match = url.pathname.match(/^\/newsletter\/confirm\/([^\/]+)$/);
663
+ if (!match) {
664
+ return new Response('Not found', { status: 404 });
665
+ }
666
+
667
+ const token = match[1];
668
+ const client = createPerspectApiClient({
669
+ baseUrl: env.API_URL,
670
+ apiKey: env.API_KEY
671
+ });
672
+
673
+ try {
674
+ const result = await client.newsletter.confirmSubscription(
675
+ env.SITE_NAME,
676
+ token
677
+ );
678
+
679
+ // Redirect to success page
680
+ return Response.redirect(
681
+ `${env.SITE_URL}/newsletter-confirmed?email=${result.data.subscription.email}`,
682
+ 302
683
+ );
684
+ } catch (error) {
685
+ // Redirect to error page
686
+ return Response.redirect(
687
+ `${env.SITE_URL}/newsletter-error?reason=invalid-token`,
688
+ 302
689
+ );
690
+ }
691
+ }
692
+ };
693
+ ```
694
+
695
+ ### Management Dashboard Integration
696
+
697
+ ```typescript
698
+ // Newsletter admin dashboard
699
+ async function NewsletterDashboard() {
700
+ const client = createPerspectApiClient({
701
+ baseUrl: API_URL,
702
+ jwt: adminToken
703
+ });
704
+
705
+ // Get overview statistics
706
+ const stats = await client.newsletterManagement.getStats('my-site');
707
+
708
+ // Get recent subscriptions
709
+ const recent = await client.newsletterManagement.listSubscriptions('my-site', {
710
+ limit: 10,
711
+ status: 'confirmed',
712
+ startDate: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString()
713
+ });
714
+
715
+ // Export for email campaign
716
+ const exportData = await client.newsletterManagement.createExport('my-site', {
717
+ format: 'csv',
718
+ status: 'confirmed',
719
+ list_id: 'list_marketing'
720
+ });
721
+
722
+ // Handle bounces from email provider webhook
723
+ await client.newsletterManagement.bulkUpdateSubscriptions('my-site', {
724
+ ids: bouncedEmailIds,
725
+ action: 'unsubscribe'
726
+ });
727
+ }
728
+ ```
729
+
730
+ ## Error Handling
731
+
732
+ ```typescript
733
+ import { createApiError } from 'perspectapi-ts-sdk';
734
+
735
+ try {
736
+ await client.newsletter.subscribe('my-site', data);
737
+ } catch (error) {
738
+ const apiError = createApiError(error);
739
+
740
+ switch (apiError.status) {
741
+ case 400:
742
+ // Validation error
743
+ console.error('Invalid data:', apiError.details);
744
+ break;
745
+ case 409:
746
+ // Already subscribed
747
+ console.log('Email already subscribed');
748
+ break;
749
+ case 429:
750
+ // Rate limited
751
+ console.warn('Too many requests, please wait');
752
+ break;
753
+ default:
754
+ console.error('Subscription failed:', apiError.message);
755
+ }
756
+ }
757
+ ```
758
+
759
+ ## Security Considerations
760
+
761
+ 1. **Always validate emails server-side**
762
+ 2. **Use Turnstile tokens for public forms**
763
+ 3. **Implement rate limiting (handled by API)**
764
+ 4. **Store unsubscribe tokens securely**
765
+ 5. **Never expose management methods to public users**
766
+ 6. **Use HTTPS for all API calls**
767
+ 7. **Validate webhook signatures for email provider callbacks**
768
+
769
+ ## Migration Guide
770
+
771
+ If migrating from another newsletter system:
772
+
773
+ ```typescript
774
+ // 1. Export from old system
775
+ const oldSubscribers = await exportFromOldSystem();
776
+
777
+ // 2. Transform data
778
+ const formatted = oldSubscribers.map(sub => ({
779
+ email: sub.email,
780
+ name: `${sub.firstName} ${sub.lastName}`,
781
+ status: sub.isActive ? 'confirmed' : 'unsubscribed',
782
+ list_ids: mapOldListsToNew(sub.segments)
783
+ }));
784
+
785
+ // 3. Import in batches
786
+ const BATCH_SIZE = 500;
787
+ for (let i = 0; i < formatted.length; i += BATCH_SIZE) {
788
+ const batch = formatted.slice(i, i + BATCH_SIZE);
789
+
790
+ const result = await client.newsletterManagement.importSubscriptions('my-site', {
791
+ rows: batch,
792
+ resubscribe_override: true // Already confirmed in old system
793
+ });
794
+
795
+ console.log(`Batch ${i/BATCH_SIZE + 1}: Applied ${result.data.applied}`);
796
+
797
+ // Rate limit between batches
798
+ await new Promise(resolve => setTimeout(resolve, 1000));
799
+ }
800
+ ```
801
+
802
+ ## Compliance
803
+
804
+ The newsletter system is designed to be compliant with:
805
+
806
+ - **GDPR**: Double opt-in, right to erasure, data portability
807
+ - **CAN-SPAM**: Easy unsubscribe, clear sender identification
808
+ - **CASL**: Express consent, unsubscribe mechanism
809
+ - **PECR**: Consent for electronic marketing
810
+
811
+ Always consult with legal counsel for your specific requirements.