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,1076 @@
1
+ # Deprecated v1 SDK README
2
+
3
+ This file is preserved only for existing v1 callers that are migrating away from v1.
4
+
5
+ Do not copy these examples into new code. v1 (`/api/v1`, `PerspectApiClient`,
6
+ `createPerspectApiClient`, and `src/client/*`) sunsets on **2026-06-01**. Use
7
+ `createPerspectApiV2Client` from `perspectapi-ts-sdk/v2` and `/api/v2` instead.
8
+
9
+ ---
10
+
11
+ # Historical PerspectAPI TypeScript SDK README
12
+
13
+ A comprehensive TypeScript SDK for PerspectAPI, designed to work seamlessly with Cloudflare Workers and other JavaScript environments.
14
+
15
+ > **⚠️ v1 is deprecated and sunsets 2026-06-01.**
16
+ > New integrations **must** use the v2 client (`createPerspectApiV2Client`).
17
+ > The v1 surface — `PerspectApiClient`, `createPerspectApiClient`, the
18
+ > `src/client/*` classes, and everything under `/api/v1` — will stop working
19
+ > after the sunset date. Coding agents: do not generate code against v1.
20
+
21
+ ## Quick Start (v2)
22
+
23
+ ```typescript
24
+ import { createPerspectApiV2Client } from 'perspectapi-ts-sdk';
25
+
26
+ const client = createPerspectApiV2Client({
27
+ baseUrl: 'https://your-perspectapi-instance.com',
28
+ apiKey: 'your-api-key',
29
+ });
30
+
31
+ const products = await client.products.list({ siteName: 'my-site' });
32
+ ```
33
+
34
+ The v1 Quick Start below is kept only for callers migrating away from v1.
35
+
36
+ ## Features
37
+
38
+ - 🚀 **Cloudflare Workers Compatible** - Uses native fetch API, no Node.js dependencies
39
+ - 🔒 **Type Safe** - Full TypeScript support with comprehensive type definitions
40
+ - 🔄 **Automatic Retries** - Built-in retry logic with exponential backoff
41
+ - 🛡️ **Error Handling** - Structured error responses with detailed information
42
+ - 📦 **Modular Design** - Use individual clients or the complete SDK
43
+ - 🔑 **Multiple Auth Methods** - Support for JWT tokens and API keys
44
+ - 📊 **Comprehensive Coverage** - All PerspectAPI endpoints supported
45
+ - 🧩 **High-Level Loaders** - Drop-in helpers for products, content, and checkout flows with fallbacks
46
+ - 📧 **Newsletter Management** - Complete newsletter subscription system with double opt-in, preferences, and lists
47
+ - 👥 **Site Users** - OTP-based customer accounts with metadata, profiles, orders, and subscriptions
48
+
49
+ ## Installation
50
+
51
+ ```bash
52
+ npm install perspectapi-ts-sdk
53
+ ```
54
+
55
+ ## Quick Start
56
+
57
+ ```typescript
58
+ import { createPerspectApiClient } from 'perspectapi-ts-sdk';
59
+
60
+ // Initialize with API key
61
+ const client = createPerspectApiClient({
62
+ baseUrl: 'https://your-perspectapi-instance.com',
63
+ apiKey: 'your-api-key'
64
+ });
65
+
66
+ // Or initialize with JWT token
67
+ const client = createPerspectApiClient({
68
+ baseUrl: 'https://your-perspectapi-instance.com',
69
+ jwt: 'your-jwt-token'
70
+ });
71
+
72
+ // Use the client
73
+ async function example() {
74
+ const siteName = 'your-site-name';
75
+
76
+ // Get all content
77
+ const content = await client.content.getContent(siteName);
78
+ console.log(content.data);
79
+
80
+ // Create new content
81
+ const newContent = await client.content.createContent({
82
+ page_title: 'Hello World',
83
+ page_content: 'This is my first post!',
84
+ page_status: 'publish',
85
+ page_type: 'post'
86
+ });
87
+
88
+ console.log(newContent.data);
89
+ }
90
+
91
+ ### High-Level Loaders (Quick Example)
92
+
93
+ ```typescript
94
+ import {
95
+ loadProducts,
96
+ loadPosts,
97
+ createCheckoutSession
98
+ } from 'perspectapi-ts-sdk';
99
+
100
+ const loaders = {
101
+ client: createPerspectApiClient({ baseUrl, apiKey }),
102
+ siteName: 'my-museum-site'
103
+ };
104
+
105
+ const products = await loadProducts(loaders);
106
+ const posts = await loadPosts(loaders);
107
+
108
+ const checkout = await createCheckoutSession({
109
+ ...loaders,
110
+ items: [{ productId: products[0].id, quantity: 1 }],
111
+ successUrl: 'https://example.com/success',
112
+ cancelUrl: 'https://example.com/cancel'
113
+ });
114
+ ```
115
+
116
+ > 📚 See [legacy-docs/loaders.md](legacy-docs/loaders.md) for full walkthroughs, including fallback data, custom logging, and Stripe price resolution.
117
+
118
+ ## Configuring Caching
119
+
120
+ Pass a `cache` block when you create the client to enable caching. The SDK falls back to an in-memory store in development; in production you can supply any adapter that satisfies the `CacheAdapter` interface.
121
+
122
+ ```ts
123
+ import { PerspectApiClient } from 'perspectapi-ts-sdk';
124
+ import { myCacheAdapter } from './cache-adapter';
125
+
126
+ const perspect = new PerspectApiClient({
127
+ baseUrl: 'https://api.perspect.comm',
128
+ apiKey: env.PERSPECT_API_KEY,
129
+ cache: {
130
+ adapter: myCacheAdapter,
131
+ defaultTtlSeconds: 300,
132
+ keyPrefix: 'storefront', // optional namespace
133
+ },
134
+ });
135
+ ```
136
+
137
+ ### Cloudflare Workers KV example
138
+
139
+ KV namespaces make a great globally distributed cache. Bind a namespace (e.g. `PERSPECT_CACHE`) in your Worker and wrap it with a small adapter:
140
+
141
+ ```ts
142
+ // worker.ts
143
+ import { PerspectApiClient, type CacheAdapter } from 'perspectapi-ts-sdk';
144
+
145
+ const kvAdapter = (kv: KVNamespace): CacheAdapter => ({
146
+ async get(key) {
147
+ const value = await kv.get(key);
148
+ return value ?? undefined;
149
+ },
150
+ async set(key, value, options) {
151
+ const ttl = options?.ttlSeconds;
152
+ await kv.put(key, value, ttl ? { expirationTtl: ttl } : undefined);
153
+ },
154
+ async delete(key) {
155
+ await kv.delete(key);
156
+ },
157
+ async deleteMany(keys) {
158
+ await Promise.all(keys.map(key => kv.delete(key)));
159
+ },
160
+ });
161
+
162
+ export default {
163
+ async fetch(request: Request, env: Env) {
164
+ const perspect = new PerspectApiClient({
165
+ baseUrl: env.PERSPECT_API_URL,
166
+ apiKey: env.PERSPECT_API_KEY,
167
+ cache: {
168
+ adapter: kvAdapter(env.PERSPECT_CACHE),
169
+ defaultTtlSeconds: 300,
170
+ keyPrefix: 'storefront',
171
+ },
172
+ });
173
+
174
+ const products = await perspect.products.getProducts('museum-indian-art');
175
+ return new Response(JSON.stringify(products.data), {
176
+ headers: { 'Content-Type': 'application/json' },
177
+ });
178
+ },
179
+ };
180
+ ```
181
+
182
+ When PerspectAPI sends a webhook—or when your Worker mutates data directly—call `perspect.cache.invalidate({ tags: [...] })` using the tags emitted by the SDK (`products:site:<site>`, `content:slug:<site>:<slug>`, `newsletter:campaigns:slug:<site>:<slug>`, etc.) so stale entries are purged immediately.
183
+
184
+ ### Webhook-driven cache invalidation
185
+
186
+ Most deployments rely on PerspectAPI webhooks to bust cache entries whenever content changes. Your app needs an HTTP endpoint that:
187
+
188
+ 1. Verifies the webhook signature (or shared secret).
189
+ 2. Maps the webhook payload to the cache tags that were used when reading data.
190
+ 3. Calls `client.cache.invalidate({ tags })`.
191
+
192
+ Below is a minimal Cloudflare Worker handler; the same flow works in Express, Fastify, etc.—just swap the request parsing.
193
+
194
+ ```ts
195
+ // worker.ts
196
+ import { PerspectApiClient } from 'perspectapi-ts-sdk';
197
+
198
+ const kvAdapter = (kv: KVNamespace) => ({
199
+ get: (key: string) => kv.get(key),
200
+ set: (key: string, value: string, options?: { ttlSeconds?: number }) =>
201
+ options?.ttlSeconds
202
+ ? kv.put(key, value, { expirationTtl: options.ttlSeconds })
203
+ : kv.put(key, value),
204
+ delete: (key: string) => kv.delete(key),
205
+ });
206
+
207
+ const perspect = new PerspectApiClient({
208
+ baseUrl: env.PERSPECT_API_URL,
209
+ apiKey: env.PERSPECT_API_KEY,
210
+ cache: { adapter: kvAdapter(env.PERSPECT_CACHE), defaultTtlSeconds: 300 },
211
+ });
212
+
213
+ type WebhookEvent =
214
+ | { type: 'product.updated'; site: string; slug?: string; id?: number }
215
+ | { type: 'content.published'; site: string; slug: string; id: number }
216
+ | { type: 'category.updated'; site: string; slug: string };
217
+
218
+ const tagMap: Record<string, (e: WebhookEvent) => string[]> = {
219
+ 'product.updated': event => [
220
+ `products`,
221
+ `products:site:${event.site}`,
222
+ event.slug ? `products:slug:${event.site}:${event.slug}` : '',
223
+ event.id ? `products:id:${event.id}` : '',
224
+ ],
225
+ 'content.published': event => [
226
+ `content`,
227
+ `content:site:${event.site}`,
228
+ `content:slug:${event.site}:${event.slug}`,
229
+ `content:id:${event.id}`,
230
+ ],
231
+ 'category.updated': event => [
232
+ `categories`,
233
+ `categories:site:${event.site}`,
234
+ `categories:product:${event.site}:${event.slug}`,
235
+ `products:category:${event.site}:${event.slug}`,
236
+ `content:category:${event.site}:${event.slug}`,
237
+ ],
238
+ };
239
+
240
+ export default {
241
+ async fetch(request: Request, env: Env) {
242
+ const url = new URL(request.url);
243
+
244
+ if (url.pathname === '/webhooks/perspect' && request.method === 'POST') {
245
+ const event = (await request.json()) as WebhookEvent;
246
+
247
+ // TODO: validate shared secret / signature before proceeding
248
+
249
+ const buildTags = tagMap[event.type];
250
+ if (buildTags) {
251
+ const tags = buildTags(event).filter(Boolean);
252
+ if (tags.length) {
253
+ await perspect.cache.invalidate({ tags });
254
+ }
255
+ }
256
+
257
+ return new Response(null, { status: 202 });
258
+ }
259
+
260
+ // ...rest of your app
261
+ return new Response('ok');
262
+ },
263
+ };
264
+ ```
265
+
266
+ > 🔁 Adjust the `WebhookEvent` union and `tagMap` to match the actual payloads you receive. PerspectAPI webhooks also carry version IDs and environment metadata that you can use for more granular targeting if needed.
267
+
268
+ ### Newsletter Publish Invalidation
269
+
270
+ Newsletter list and campaign reads are cache-aware:
271
+
272
+ ```ts
273
+ const campaigns = await perspect.newsletter.getPublishedCampaigns('museum-indian-art', {
274
+ page: 1,
275
+ limit: 20,
276
+ });
277
+ const campaign = await perspect.newsletter.getPublishedCampaignBySlug(
278
+ 'museum-indian-art',
279
+ 'spring-launch',
280
+ { slugPrefix: 'updates' }
281
+ );
282
+ ```
283
+
284
+ For webhook handling, use the newsletter helper so invalidation happens only when campaigns are published (not when drafts are created):
285
+
286
+ ```ts
287
+ const result = await perspect.newsletter.invalidatePublishedCampaignCacheFromWebhook(
288
+ webhookPayload,
289
+ 'museum-indian-art'
290
+ );
291
+
292
+ if (result.invalidated) {
293
+ console.log(result.tags);
294
+ }
295
+ ```
296
+
297
+ ## Image Transformations
298
+
299
+ The SDK includes utilities for transforming images using Cloudflare's Image Resizing service:
300
+
301
+ ```typescript
302
+ import { transformMediaItem, buildImageUrl } from 'perspectapi-ts-sdk';
303
+
304
+ // Get a product with media
305
+ const product = await client.products.getProduct('mysite', 123);
306
+ const media = product.data.media?.[0];
307
+
308
+ if (media) {
309
+ // Generate all responsive sizes automatically
310
+ const urls = transformMediaItem('https://api.perspect.comm', media);
311
+
312
+ console.log(urls.thumbnail); // 150x150 cover crop
313
+ console.log(urls.small); // 400px wide
314
+ console.log(urls.medium); // 800px wide
315
+ console.log(urls.large); // 1200px wide
316
+ console.log(urls.original); // Original with auto format
317
+ }
318
+
319
+ // Or build custom transformations
320
+ const customUrl = buildImageUrl(
321
+ 'https://api.perspect.comm',
322
+ 'media/mysite/photo.jpg',
323
+ {
324
+ width: 400,
325
+ height: 300,
326
+ fit: 'cover',
327
+ format: 'webp',
328
+ quality: 85
329
+ }
330
+ );
331
+ ```
332
+
333
+ **Features:**
334
+ - ✅ On-the-fly image resizing (no pre-generated thumbnails)
335
+ - ✅ Automatic format optimization (WebP/AVIF)
336
+ - ✅ Responsive image support with srcset
337
+ - ✅ CDN caching at the edge
338
+ - ✅ Bandwidth savings
339
+
340
+ > 📚 See [legacy-docs/image-transforms.md](legacy-docs/image-transforms.md) for complete documentation, examples, and best practices.
341
+
342
+ ## High-Level Data Loaders
343
+
344
+ The SDK now ships with convenience loaders that wrap the lower-level REST clients. They help you:
345
+
346
+ - normalise products (including nested media arrays and Stripe IDs)
347
+ - search products server-side with limit/offset support and filter by category names or IDs (no hard-coded IDs)
348
+ - fetch published posts/pages with sensible defaults
349
+ - fall back to sample data when PerspectAPI is not configured (useful for local previews)
350
+ - create Stripe checkout sessions by automatically resolving price IDs
351
+
352
+ All loaders accept a shared `LoaderOptions` object:
353
+
354
+ ```typescript
355
+ import {
356
+ loadProducts,
357
+ loadProductBySlug,
358
+ loadPosts,
359
+ createCheckoutSession,
360
+ type LoaderOptions
361
+ } from 'perspectapi-ts-sdk';
362
+
363
+ const options: LoaderOptions = {
364
+ client: createPerspectApiClient({ baseUrl, apiKey }),
365
+ siteName: 'museum-indian-art',
366
+ logger: console, // optional
367
+ fallbackProducts: [], // optional
368
+ fallbackPosts: [] // optional
369
+ };
370
+
371
+ const products = await loadProducts(options);
372
+ const single = await loadProductBySlug({ ...options, slug: 'sunset-painting' });
373
+ const posts = await loadPosts(options);
374
+
375
+ await createCheckoutSession({
376
+ ...options,
377
+ items: [{ productId: products[0].id, quantity: 1 }],
378
+ successUrl: 'https://example.com/success',
379
+ cancelUrl: 'https://example.com/cancel'
380
+ });
381
+
382
+ // Filter products by multiple category names/IDs while combining search
383
+ const decorPillows = await loadProducts({
384
+ ...options,
385
+ category: ['Home Decor', 'Seasonal Sale'],
386
+ categoryIds: [12],
387
+ search: 'pillow',
388
+ offset: 24,
389
+ limit: 12
390
+ });
391
+ ```
392
+
393
+ > More real-world scenarios—including Cloudflare Workers, Remix/Next.js loaders, custom logger implementations, and checkout price resolution—are documented in [legacy-docs/loaders.md](legacy-docs/loaders.md).
394
+
395
+ ## Authentication
396
+
397
+ ### API Key Authentication
398
+
399
+ ```typescript
400
+ const client = createPerspectApiClient({
401
+ baseUrl: 'https://api.example.com',
402
+ apiKey: 'pk_your_api_key_here'
403
+ });
404
+ ```
405
+
406
+ ### JWT Token Authentication
407
+
408
+ ```typescript
409
+ const client = createPerspectApiClient({
410
+ baseUrl: 'https://api.example.com',
411
+ jwt: 'your.jwt.token'
412
+ });
413
+
414
+ // Or authenticate after initialization
415
+ await client.auth.signIn({
416
+ email: 'user@example.com',
417
+ password: 'password'
418
+ });
419
+ ```
420
+
421
+ ### Dynamic Authentication
422
+
423
+ ```typescript
424
+ const client = createPerspectApiClient({
425
+ baseUrl: 'https://api.example.com'
426
+ });
427
+
428
+ // Set authentication later
429
+ client.setApiKey('pk_your_api_key');
430
+ // or
431
+ client.setAuth('your.jwt.token');
432
+
433
+ // Clear authentication
434
+ client.clearAuth();
435
+ ```
436
+
437
+ ## API Reference
438
+
439
+ ### Content Management
440
+
441
+ ```typescript
442
+ // Get all content with pagination
443
+ const content = await client.content.getContent('your-site-name', {
444
+ page: 1,
445
+ limit: 20,
446
+ page_status: 'publish',
447
+ page_type: 'post',
448
+ slug_prefix: 'blog' // Optional: filter by slug prefix
449
+ });
450
+
451
+ // Get content by ID
452
+ const post = await client.content.getContentById(123);
453
+
454
+ // Get content by slug
455
+ const page = await client.content.getContentBySlug('your-site-name', 'about-us');
456
+
457
+ // Get content by category slug
458
+ const categoryContent = await client.content.getContentByCategorySlug(
459
+ 'your-site-name',
460
+ 'news',
461
+ {
462
+ page: 1,
463
+ limit: 20,
464
+ page_type: 'post'
465
+ }
466
+ );
467
+
468
+ // Create new content
469
+ const newPost = await client.content.createContent({
470
+ page_title: 'My New Post',
471
+ page_content: '<p>Content goes here</p>',
472
+ content_markdown: '# My New Post\n\nContent goes here',
473
+ page_status: 'draft',
474
+ page_type: 'post',
475
+ slug: 'my-new-post'
476
+ });
477
+
478
+ // Update content
479
+ const updated = await client.content.updateContent(123, {
480
+ page_title: 'Updated Title',
481
+ page_status: 'publish'
482
+ });
483
+
484
+ // Delete content
485
+ await client.content.deleteContent(123);
486
+
487
+ // Publish/unpublish content
488
+ await client.content.publishContent(123);
489
+ await client.content.unpublishContent(123);
490
+ ```
491
+
492
+ ### Products & E-commerce
493
+
494
+ ```typescript
495
+ // Get all products
496
+ const products = await client.products.getProducts('my-site', {
497
+ page: 1,
498
+ limit: 20,
499
+ isActive: true,
500
+ slug_prefix: 'shop' // Optional: filter by slug prefix
501
+ });
502
+
503
+ // Create new product
504
+ const product = await client.products.createProduct({
505
+ name: 'Amazing Product',
506
+ description: 'Product description',
507
+ price: 29.99,
508
+ currency: 'USD',
509
+ sku: 'PROD-001'
510
+ });
511
+
512
+ // Update product inventory
513
+ await client.products.updateProductInventory(123, {
514
+ quantity: 10,
515
+ operation: 'add',
516
+ reason: 'Restocked'
517
+ });
518
+
519
+ // Get product by slug and site name (NEW!)
520
+ const productBySlug = await client.products.getProductBySlug('my-site', 'awesome-laptop');
521
+ console.log('Product:', productBySlug.data?.name);
522
+ console.log('Variants:', productBySlug.data?.variants);
523
+
524
+ // Get products by category slug (NEW!)
525
+ const categoryProducts = await client.products.getProductsByCategorySlug(
526
+ 'my-site',
527
+ 'electronics',
528
+ {
529
+ page: 1,
530
+ limit: 20,
531
+ published: true,
532
+ search: 'phone'
533
+ }
534
+ );
535
+ console.log('Products:', categoryProducts.data?.data);
536
+ console.log('Category info:', categoryProducts.data?.category);
537
+ ```
538
+
539
+ ### Organizations & Sites
540
+
541
+ ```typescript
542
+ // Get organizations
543
+ const orgs = await client.organizations.getOrganizations();
544
+
545
+ // Create new organization
546
+ const org = await client.organizations.createOrganization({
547
+ name: 'My Company',
548
+ description: 'Company description'
549
+ });
550
+
551
+ // Get sites
552
+ const sites = await client.sites.getSites({
553
+ organizationId: 1
554
+ });
555
+
556
+ // Create new site
557
+ const site = await client.sites.createSite({
558
+ name: 'My Website',
559
+ domain: 'example.com',
560
+ organizationId: 1
561
+ });
562
+ ```
563
+
564
+ ### API Key Management
565
+
566
+ ```typescript
567
+ // Get all API keys
568
+ const apiKeys = await client.apiKeys.getApiKeys();
569
+
570
+ // Create new API key
571
+ const newKey = await client.apiKeys.createApiKey({
572
+ name: 'Frontend App Key',
573
+ description: 'API key for frontend application',
574
+ permissions: ['content:read', 'products:read'],
575
+ expiresAt: '2024-12-31T23:59:59Z'
576
+ });
577
+
578
+ console.log('New API key:', newKey.data.key);
579
+
580
+ // Test API key validity
581
+ const testResult = await client.apiKeys.testApiKey('pk_test_key');
582
+ console.log('Key valid:', testResult.data.valid);
583
+ ```
584
+
585
+ ### Webhooks
586
+
587
+ ```typescript
588
+ // Get all webhooks
589
+ const webhooks = await client.webhooks.getWebhooks();
590
+
591
+ // Create new webhook
592
+ const webhook = await client.webhooks.createWebhook({
593
+ name: 'Order Notifications',
594
+ url: 'https://myapp.com/webhooks/orders',
595
+ provider: 'stripe',
596
+ events: ['payment_intent.succeeded', 'payment_intent.payment_failed'],
597
+ isActive: true
598
+ });
599
+
600
+ // Test webhook
601
+ const testResult = await client.webhooks.testWebhook(webhook.data.id);
602
+ console.log('Webhook test:', testResult.data);
603
+ ```
604
+
605
+ ### Checkout & Payments
606
+
607
+ ```typescript
608
+ // Create Stripe checkout session
609
+ const session = await client.checkout.createCheckoutSession({
610
+ priceId: 'price_1234567890',
611
+ successUrl: 'https://myapp.com/success',
612
+ cancelUrl: 'https://myapp.com/cancel',
613
+ customerEmail: 'customer@example.com',
614
+ currency: 'usd',
615
+ shipping_amount: 500,
616
+ shipping_address: {
617
+ country: 'US',
618
+ state: 'CA',
619
+ postal_code: '94110'
620
+ },
621
+ billing_address: {
622
+ country: 'US',
623
+ state: 'CA',
624
+ postal_code: '94110'
625
+ },
626
+ tax: {
627
+ strategy: 'manual_rates',
628
+ customer_identifier: 'acct-123',
629
+ customer_display_name: 'Acme Corp',
630
+ customer_exemption: {
631
+ status: 'exempt',
632
+ tax_id: '99-1234567',
633
+ tax_id_type: 'ein'
634
+ }
635
+ }
636
+ });
637
+
638
+ // Redirect user to checkout
639
+ window.location.href = session.data.url;
640
+
641
+ // Get checkout session status
642
+ const sessionStatus = await client.checkout.getCheckoutSession('cs_test_123');
643
+ console.log(sessionStatus.data.tax?.amount); // Display assessed tax (if calculated)
644
+ ```
645
+
646
+ ### Contact Forms
647
+
648
+ ```typescript
649
+ const siteName = 'your-site-name';
650
+
651
+ // Submit contact form
652
+ const submission = await client.contact.submitContact(siteName, {
653
+ name: 'John Doe',
654
+ email: 'john@example.com',
655
+ subject: 'Question about pricing',
656
+ message: 'I have a question about your pricing plans.',
657
+ turnstileToken: 'turnstile-response-token'
658
+ });
659
+
660
+ // Get contact submissions (admin only)
661
+ const submissions = await client.contact.getContactSubmissions(siteName, {
662
+ status: 'unread',
663
+ page: 1,
664
+ limit: 50
665
+ });
666
+
667
+ // Update submission status
668
+ await client.contact.updateContactStatus(siteName, submission.data.id, 'read');
669
+ ```
670
+
671
+ ### Newsletter Subscriptions
672
+
673
+ ```typescript
674
+ const siteName = 'your-site-name';
675
+
676
+ // Subscribe to newsletter (with double opt-in)
677
+ const subscription = await client.newsletter.subscribe(siteName, {
678
+ email: 'subscriber@example.com',
679
+ name: 'Jane Doe',
680
+ list_ids: ['list_default'], // Optional: subscribe to specific lists
681
+ frequency: 'weekly', // instant, daily, weekly, monthly
682
+ topics: ['news', 'updates'], // Optional: topic preferences
683
+ double_opt_in: true, // Default: true for GDPR compliance
684
+ turnstile_token: 'token' // Optional: Turnstile verification
685
+ });
686
+
687
+ // Confirm subscription via email token
688
+ const confirmed = await client.newsletter.confirmSubscription(
689
+ siteName,
690
+ 'confirmation-token-from-email'
691
+ );
692
+
693
+ // Unsubscribe
694
+ const unsubscribed = await client.newsletter.unsubscribe(siteName, {
695
+ email: 'subscriber@example.com',
696
+ reason: 'Too many emails' // Optional feedback
697
+ });
698
+
699
+ // One-click unsubscribe (from email link)
700
+ await client.newsletter.unsubscribeByToken(siteName, 'unsubscribe-token');
701
+
702
+ // Update preferences
703
+ await client.newsletter.updatePreferences(siteName, 'subscriber@example.com', {
704
+ frequency: 'monthly',
705
+ topics: ['product-updates'],
706
+ email_format: 'html', // html, text, or both
707
+ timezone: 'America/New_York',
708
+ track_opens: false,
709
+ track_clicks: false
710
+ });
711
+
712
+ // Check subscription status
713
+ const status = await client.newsletter.getStatus(siteName, 'subscriber@example.com');
714
+ console.log('Subscribed:', status.data.subscribed);
715
+ console.log('Status:', status.data.status); // pending, confirmed, unsubscribed
716
+
717
+ // Get available newsletter lists
718
+ const lists = await client.newsletter.getLists(siteName);
719
+ console.log('Available lists:', lists.data.lists);
720
+ ```
721
+
722
+ #### Newsletter Management APIs
723
+
724
+ `client.newsletter` is now public-only (subscribe/confirm/unsubscribe/preferences/public lists/published campaigns).
725
+ For management operations, use `client.newsletterManagement`.
726
+
727
+ ```typescript
728
+ // Sync subscriber by email (canonical create/update path)
729
+ const sync = await client.newsletterManagement.syncSubscription(siteName, {
730
+ email: 'user@example.com',
731
+ name: 'User Name',
732
+ list_ids: ['list_default'],
733
+ resubscribe_override: false
734
+ });
735
+ console.log(sync.data.code); // CREATED, UPDATED, RESUBSCRIBED, ALREADY_UNSUBSCRIBED
736
+
737
+ // List subscribers
738
+ const subscriptions = await client.newsletterManagement.listSubscriptions(siteName, {
739
+ page: 1,
740
+ limit: 100,
741
+ status: 'confirmed'
742
+ });
743
+
744
+ // Import with override controls (row-level override wins)
745
+ await client.newsletterManagement.importSubscriptions(siteName, {
746
+ resubscribe_override: false,
747
+ rows: [
748
+ { email: 'user1@example.com', list_ids: ['list_default'] },
749
+ { email: 'user2@example.com', resubscribe_override: true }
750
+ ]
751
+ });
752
+
753
+ // List + campaign management
754
+ await client.newsletterManagement.createList(siteName, {
755
+ list_name: 'VIP Customers',
756
+ slug: 'vip-customers',
757
+ is_public: false
758
+ });
759
+
760
+ await client.newsletterManagement.createCampaign(siteName, {
761
+ campaign_name: 'Spring Update',
762
+ subject: 'Spring Update',
763
+ markdown_content: '# Hello',
764
+ status: 'draft'
765
+ });
766
+
767
+ // Exports (csv/json only; xlsx returns UNSUPPORTED_FORMAT)
768
+ const exportData = await client.newsletterManagement.createExport(siteName, {
769
+ format: 'csv'
770
+ });
771
+ console.log(exportData.data.downloadUrl);
772
+ ```
773
+
774
+ > 📚 **For complete newsletter documentation**, see [legacy-docs/newsletter.md](legacy-docs/newsletter.md)
775
+
776
+ ### Site Users (Customer Accounts)
777
+
778
+ Site users are per-site customer accounts with OTP-based authentication, separate from admin users.
779
+
780
+ ```typescript
781
+ const siteName = 'your-site-name';
782
+
783
+ // Request OTP for login/signup (with optional metadata)
784
+ await client.siteUsers.requestOtp(
785
+ siteName,
786
+ {
787
+ email: 'user@example.com',
788
+ waitlist: true, // Optional: mark as waitlist signup
789
+ metadata: { // Optional: set metadata at signup time
790
+ signupSource: 'landing-page',
791
+ referralCode: 'FRIEND123',
792
+ interests: ['product-updates']
793
+ }
794
+ },
795
+ csrfToken
796
+ );
797
+
798
+ // Verify OTP and get JWT token
799
+ const response = await client.siteUsers.verifyOtp(
800
+ siteName,
801
+ { email: 'user@example.com', code: '123456' },
802
+ csrfToken
803
+ );
804
+
805
+ const { token, user } = response.data;
806
+ client.setAuth(token); // Set auth for subsequent requests
807
+
808
+ // Get current user profile
809
+ const { data } = await client.siteUsers.getMe(siteName);
810
+ console.log(data.user); // Basic user fields + metadata
811
+ console.log(data.profile); // Key-value profile data
812
+
813
+ // Update user profile and metadata
814
+ await client.siteUsers.updateMe(
815
+ siteName,
816
+ {
817
+ first_name: 'John',
818
+ last_name: 'Doe',
819
+ metadata: {
820
+ preferences: { theme: 'dark' },
821
+ tags: ['premium'],
822
+ customField: 'value'
823
+ }
824
+ },
825
+ csrfToken
826
+ );
827
+
828
+ // Set profile key-values (phone, addresses, etc.)
829
+ await client.siteUsers.setProfileValue(
830
+ siteName,
831
+ 'phone',
832
+ '+1-555-0123',
833
+ csrfToken
834
+ );
835
+
836
+ await client.siteUsers.setProfileValue(
837
+ siteName,
838
+ 'address_shipping',
839
+ JSON.stringify({
840
+ line1: '123 Main St',
841
+ city: 'San Francisco',
842
+ state: 'CA',
843
+ postal_code: '94102',
844
+ country: 'US'
845
+ }),
846
+ csrfToken
847
+ );
848
+
849
+ // Get order history
850
+ const orders = await client.siteUsers.getOrders(siteName, {
851
+ limit: 50,
852
+ offset: 0
853
+ });
854
+
855
+ // Get subscriptions
856
+ const subscriptions = await client.siteUsers.getSubscriptions(siteName);
857
+
858
+ // Cancel at end of billing period (default)
859
+ await client.siteUsers.cancelSubscription(siteName, 'sub_123', csrfToken);
860
+
861
+ // Cancel immediately
862
+ await client.siteUsers.cancelSubscription(
863
+ siteName,
864
+ 'sub_123',
865
+ csrfToken,
866
+ { mode: 'immediate' }
867
+ );
868
+
869
+ // Cancel at a scheduled time (absolute)
870
+ await client.siteUsers.cancelSubscription(
871
+ siteName,
872
+ 'sub_123',
873
+ csrfToken,
874
+ { mode: 'scheduled', cancel_at: '2026-05-01T15:30:00Z' }
875
+ );
876
+
877
+ // Cancel after a configured delay (days/hours/minutes)
878
+ await client.siteUsers.cancelSubscription(
879
+ siteName,
880
+ 'sub_123',
881
+ csrfToken,
882
+ { mode: 'scheduled', delay_days: 7, delay_hours: 2 }
883
+ );
884
+
885
+ // Logout
886
+ await client.siteUsers.logout(siteName);
887
+ client.setAuth(null);
888
+ ```
889
+
890
+ > 📚 **For complete site users documentation** including waitlist management, metadata patterns, cross-domain authentication, and complete examples, see [legacy-docs/site-users.md](legacy-docs/site-users.md)
891
+
892
+ ## Configuration Options
893
+
894
+ ```typescript
895
+ interface PerspectApiConfig {
896
+ baseUrl: string; // Required: API base URL
897
+ apiKey?: string; // Optional: API key for authentication
898
+ jwt?: string; // Optional: JWT token for authentication
899
+ timeout?: number; // Optional: Request timeout in ms (default: 30000)
900
+ retries?: number; // Optional: Number of retries (default: 3)
901
+ headers?: Record<string, string>; // Optional: Additional headers
902
+ }
903
+ ```
904
+
905
+ ## Error Handling
906
+
907
+ The SDK provides structured error handling:
908
+
909
+ ```typescript
910
+ import { createApiError } from 'perspectapi-ts-sdk';
911
+
912
+ try {
913
+ const content = await client.content.getContentById(999);
914
+ } catch (error) {
915
+ const apiError = createApiError(error);
916
+
917
+ console.log('Error message:', apiError.message);
918
+ console.log('Status code:', apiError.status);
919
+ console.log('Error code:', apiError.code);
920
+ console.log('Details:', apiError.details);
921
+ }
922
+ ```
923
+
924
+ ## Cloudflare Workers Usage
925
+
926
+ The SDK is fully compatible with Cloudflare Workers:
927
+
928
+ ```typescript
929
+ // worker.ts
930
+ import { createPerspectApiClient } from 'perspectapi-ts-sdk';
931
+
932
+ export default {
933
+ async fetch(request: Request, env: any): Promise<Response> {
934
+ const client = createPerspectApiClient({
935
+ baseUrl: env.PERSPECT_API_URL,
936
+ apiKey: env.PERSPECT_API_KEY
937
+ });
938
+
939
+ try {
940
+ const content = await client.content.getContent(env.PERSPECT_SITE_NAME, { limit: 10 });
941
+
942
+ return new Response(JSON.stringify(content.data), {
943
+ headers: { 'Content-Type': 'application/json' }
944
+ });
945
+ } catch (error) {
946
+ return new Response('Error fetching content', { status: 500 });
947
+ }
948
+ }
949
+ };
950
+ ```
951
+
952
+ ## Advanced Usage
953
+
954
+ ### Using Individual Clients
955
+
956
+ ```typescript
957
+ import { HttpClient, ContentClient } from 'perspectapi-ts-sdk';
958
+
959
+ // Create HTTP client
960
+ const http = new HttpClient({
961
+ baseUrl: 'https://api.example.com',
962
+ apiKey: 'your-api-key'
963
+ });
964
+
965
+ // Use individual client
966
+ const contentClient = new ContentClient(http);
967
+ const content = await contentClient.getContent('your-site-name');
968
+ ```
969
+
970
+ ### Custom Headers
971
+
972
+ ```typescript
973
+ const client = createPerspectApiClient({
974
+ baseUrl: 'https://api.example.com',
975
+ apiKey: 'your-api-key',
976
+ headers: {
977
+ 'X-Custom-Header': 'custom-value',
978
+ 'User-Agent': 'MyApp/1.0.0'
979
+ }
980
+ });
981
+ ```
982
+
983
+ ### Timeout and Retries
984
+
985
+ ```typescript
986
+ const client = createPerspectApiClient({
987
+ baseUrl: 'https://api.example.com',
988
+ apiKey: 'your-api-key',
989
+ timeout: 60000, // 60 seconds
990
+ retries: 5 // Retry up to 5 times
991
+ });
992
+ ```
993
+
994
+ ## Slug Prefix Filtering
995
+
996
+ The SDK supports filtering content and products by `slug_prefix` to organize your content by URL structure:
997
+
998
+ ```typescript
999
+ // Filter blog posts
1000
+ const blogPosts = await client.content.getContent('mysite', {
1001
+ slug_prefix: 'blog', // /blog/post-1, /blog/post-2
1002
+ page_status: 'publish'
1003
+ });
1004
+
1005
+ // Filter news articles
1006
+ const news = await client.content.getContent('mysite', {
1007
+ slug_prefix: 'news', // /news/article-1, /news/article-2
1008
+ page_status: 'publish'
1009
+ });
1010
+
1011
+ // Filter shop products
1012
+ const shopProducts = await client.products.getProducts('mysite', {
1013
+ slug_prefix: 'shop', // /shop/product-1, /shop/product-2
1014
+ isActive: true
1015
+ });
1016
+
1017
+ // Filter artwork products
1018
+ const artwork = await client.products.getProducts('mysite', {
1019
+ slug_prefix: 'artwork', // /artwork/painting-1, /artwork/sculpture-1
1020
+ isActive: true
1021
+ });
1022
+
1023
+ // Combine with other filters
1024
+ const results = await client.content.getContent('mysite', {
1025
+ slug_prefix: 'blog',
1026
+ page_status: 'publish',
1027
+ page_type: 'post',
1028
+ search: 'javascript', // Search within blog posts only
1029
+ page: 1,
1030
+ limit: 20
1031
+ });
1032
+ ```
1033
+
1034
+ **Use Cases:**
1035
+ - Organize blog posts, news, and documentation separately
1036
+ - Separate shop products from artwork or digital downloads
1037
+ - Create multi-section websites with distinct URL structures
1038
+ - Filter content/products by section for navigation
1039
+
1040
+ **See `examples/slug-prefix-examples.ts` in this deprecated archive for 20+ historical v1 examples.**
1041
+
1042
+ ## TypeScript Support
1043
+
1044
+ The SDK is written in TypeScript and provides comprehensive type definitions:
1045
+
1046
+ ```typescript
1047
+ import type {
1048
+ Content,
1049
+ Product,
1050
+ ApiResponse,
1051
+ PaginatedResponse
1052
+ } from 'perspectapi-ts-sdk';
1053
+
1054
+ // Fully typed responses
1055
+ const content: ApiResponse<Content> = await client.content.getContentById(123);
1056
+ const products: PaginatedResponse<Product> = await client.products.getProducts('your-site-name');
1057
+ ```
1058
+
1059
+ ## Contributing
1060
+
1061
+ 1. Fork the repository
1062
+ 2. Create your feature branch (`git checkout -b feature/amazing-feature`)
1063
+ 3. Commit your changes (`git commit -m 'Add some amazing feature'`)
1064
+ 4. Push to the branch (`git push origin feature/amazing-feature`)
1065
+ 5. Open a Pull Request
1066
+
1067
+ ## License
1068
+
1069
+ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
1070
+
1071
+ ## Support
1072
+
1073
+ - 📖 [Documentation](https://docs.perspectapi.com)
1074
+ - 🐛 [Issue Tracker](https://github.com/perspectapi/perspectapi-ts-sdk/issues)
1075
+ - 💬 [Discussions](https://github.com/perspectapi/perspectapi-ts-sdk/discussions)
1076
+ - 📧 [Email Support](mailto:support@perspectapi.com)