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.
- package/README.md +46 -1011
- package/dist/chunk-K3T2AFYA.mjs +1393 -0
- package/dist/index-CWvUyMt3.d.mts +2224 -0
- package/dist/index-CWvUyMt3.d.ts +2224 -0
- package/dist/index.d.mts +130 -2221
- package/dist/index.d.ts +130 -2221
- package/dist/index.js +8 -2
- package/dist/index.mjs +13 -1364
- package/dist/v2/index.d.mts +1 -0
- package/dist/v2/index.d.ts +1 -0
- package/dist/v2/index.js +1419 -0
- package/dist/v2/index.mjs +40 -0
- package/docs/README.md +15 -0
- package/docs/v1-deprecated/README.md +9 -0
- package/docs/v1-deprecated/examples/README.md +324 -0
- package/docs/v1-deprecated/examples/basic-usage.ts +258 -0
- package/docs/v1-deprecated/examples/cloudflare-worker.ts +274 -0
- package/docs/v1-deprecated/examples/content-query-with-slug-prefix.ts +237 -0
- package/docs/v1-deprecated/examples/image-transforms.ts +200 -0
- package/docs/v1-deprecated/examples/site-user-checkout.ts +186 -0
- package/docs/v1-deprecated/examples/slug-prefix-examples.ts +491 -0
- package/docs/v1-deprecated/legacy-docs/caching.md +667 -0
- package/docs/v1-deprecated/legacy-docs/contact.md +1396 -0
- package/docs/v1-deprecated/legacy-docs/csrf-protection.md +664 -0
- package/docs/v1-deprecated/legacy-docs/image-transforms.md +523 -0
- package/docs/v1-deprecated/legacy-docs/loaders.md +304 -0
- package/docs/v1-deprecated/legacy-docs/newsletter.md +811 -0
- package/docs/v1-deprecated/legacy-docs/site-users.md +817 -0
- package/docs/v1-deprecated/legacy-notes/CHANGELOG-CHECKOUT.md +143 -0
- package/docs/v1-deprecated/legacy-notes/CSRF-CHECKOUT.md +271 -0
- package/docs/v1-deprecated/legacy-notes/IMAGE_TRANSFORMS_PORT.md +298 -0
- package/docs/v1-deprecated/sdk-readme.md +1076 -0
- package/examples/README.md +19 -0
- package/examples/basic-v2.ts +37 -0
- package/llms.txt +25 -0
- package/package.json +18 -7
- package/src/client/api-keys-client.ts +4 -0
- package/src/client/auth-client.ts +4 -0
- package/src/client/base-client.ts +7 -0
- package/src/client/bundles-client.ts +4 -0
- package/src/client/categories-client.ts +4 -0
- package/src/client/checkout-client.ts +4 -0
- package/src/client/contact-client.ts +4 -0
- package/src/client/content-client.ts +4 -0
- package/src/client/newsletter-client.ts +4 -0
- package/src/client/newsletter-management-client.ts +4 -0
- package/src/client/organizations-client.ts +4 -0
- package/src/client/products-client.ts +4 -0
- package/src/client/site-users-client.ts +10 -1
- package/src/client/sites-client.ts +4 -0
- package/src/client/webhooks-client.ts +4 -0
- package/src/deprecation.ts +2 -1
- package/src/index.ts +2 -1
- package/src/loaders.ts +59 -0
- package/src/perspect-api-client.ts +2 -2
- package/src/v2/client/orders-client.ts +6 -1
- package/src/v2/types.ts +3 -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.
|