perspectapi-ts-sdk 6.5.9 → 7.0.0

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