Package Exports
- perspectapi-ts-sdk
Readme
PerspectAPI TypeScript SDK
A comprehensive TypeScript SDK for PerspectAPI, designed to work seamlessly with Cloudflare Workers and other JavaScript environments.
Features
- 🚀 Cloudflare Workers Compatible - Uses native fetch API, no Node.js dependencies
- 🔒 Type Safe - Full TypeScript support with comprehensive type definitions
- 🔄 Automatic Retries - Built-in retry logic with exponential backoff
- 🛡️ Error Handling - Structured error responses with detailed information
- 📦 Modular Design - Use individual clients or the complete SDK
- 🔑 Multiple Auth Methods - Support for JWT tokens and API keys
- 📊 Comprehensive Coverage - All PerspectAPI endpoints supported
- 🧩 High-Level Loaders - Drop-in helpers for products, content, and checkout flows with fallbacks
- 📧 Newsletter Management - Complete newsletter subscription system with double opt-in, preferences, and lists
- 👥 Site Users - OTP-based customer accounts with metadata, profiles, orders, and subscriptions
Installation
npm install perspectapi-ts-sdkQuick Start
import { createPerspectApiClient } from 'perspectapi-ts-sdk';
// Initialize with API key
const client = createPerspectApiClient({
baseUrl: 'https://your-perspectapi-instance.com',
apiKey: 'your-api-key'
});
// Or initialize with JWT token
const client = createPerspectApiClient({
baseUrl: 'https://your-perspectapi-instance.com',
jwt: 'your-jwt-token'
});
// Use the client
async function example() {
const siteName = 'your-site-name';
// Get all content
const content = await client.content.getContent(siteName);
console.log(content.data);
// Create new content
const newContent = await client.content.createContent({
page_title: 'Hello World',
page_content: 'This is my first post!',
page_status: 'publish',
page_type: 'post'
});
console.log(newContent.data);
}
### High-Level Loaders (Quick Example)
```typescript
import {
loadProducts,
loadPosts,
createCheckoutSession
} from 'perspectapi-ts-sdk';
const loaders = {
client: createPerspectApiClient({ baseUrl, apiKey }),
siteName: 'my-museum-site'
};
const products = await loadProducts(loaders);
const posts = await loadPosts(loaders);
const checkout = await createCheckoutSession({
...loaders,
items: [{ productId: products[0].id, quantity: 1 }],
successUrl: 'https://example.com/success',
cancelUrl: 'https://example.com/cancel'
});📚 See docs/loaders.md for full walkthroughs, including fallback data, custom logging, and Stripe price resolution.
Configuring Caching
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.
import { PerspectApiClient } from 'perspectapi-ts-sdk';
import { myCacheAdapter } from './cache-adapter';
const perspect = new PerspectApiClient({
baseUrl: 'https://api.perspect.co',
apiKey: env.PERSPECT_API_KEY,
cache: {
adapter: myCacheAdapter,
defaultTtlSeconds: 300,
keyPrefix: 'storefront', // optional namespace
},
});Cloudflare Workers KV example
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:
// worker.ts
import { PerspectApiClient, type CacheAdapter } from 'perspectapi-ts-sdk';
const kvAdapter = (kv: KVNamespace): CacheAdapter => ({
async get(key) {
const value = await kv.get(key);
return value ?? undefined;
},
async set(key, value, options) {
const ttl = options?.ttlSeconds;
await kv.put(key, value, ttl ? { expirationTtl: ttl } : undefined);
},
async delete(key) {
await kv.delete(key);
},
async deleteMany(keys) {
await Promise.all(keys.map(key => kv.delete(key)));
},
});
export default {
async fetch(request: Request, env: Env) {
const perspect = new PerspectApiClient({
baseUrl: env.PERSPECT_API_URL,
apiKey: env.PERSPECT_API_KEY,
cache: {
adapter: kvAdapter(env.PERSPECT_CACHE),
defaultTtlSeconds: 300,
keyPrefix: 'storefront',
},
});
const products = await perspect.products.getProducts('museum-indian-art');
return new Response(JSON.stringify(products.data), {
headers: { 'Content-Type': 'application/json' },
});
},
};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>, content:category:<site>:<category_slug>, etc.) so stale entries are purged immediately.
Webhook-driven cache invalidation
Most deployments rely on PerspectAPI webhooks to bust cache entries whenever content changes. Your app needs an HTTP endpoint that:
- Verifies the webhook signature (or shared secret).
- Maps the webhook payload to the cache tags that were used when reading data.
- Calls
client.cache.invalidate({ tags }).
Below is a minimal Cloudflare Worker handler; the same flow works in Express, Fastify, etc.—just swap the request parsing.
// worker.ts
import { PerspectApiClient } from 'perspectapi-ts-sdk';
const kvAdapter = (kv: KVNamespace) => ({
get: (key: string) => kv.get(key),
set: (key: string, value: string, options?: { ttlSeconds?: number }) =>
options?.ttlSeconds
? kv.put(key, value, { expirationTtl: options.ttlSeconds })
: kv.put(key, value),
delete: (key: string) => kv.delete(key),
});
const perspect = new PerspectApiClient({
baseUrl: env.PERSPECT_API_URL,
apiKey: env.PERSPECT_API_KEY,
cache: { adapter: kvAdapter(env.PERSPECT_CACHE), defaultTtlSeconds: 300 },
});
type WebhookEvent =
| { type: 'product.updated'; site: string; slug?: string; id?: number }
| { type: 'content.published'; site: string; slug: string; id: number }
| { type: 'category.updated'; site: string; slug: string };
const tagMap: Record<string, (e: WebhookEvent) => string[]> = {
'product.updated': event => [
`products`,
`products:site:${event.site}`,
event.slug ? `products:slug:${event.site}:${event.slug}` : '',
event.id ? `products🆔${event.id}` : '',
],
'content.published': event => [
`content`,
`content:site:${event.site}`,
`content:slug:${event.site}:${event.slug}`,
`content🆔${event.id}`,
],
'category.updated': event => [
`categories`,
`categories:site:${event.site}`,
`categories:product:${event.site}:${event.slug}`,
`products:category:${event.site}:${event.slug}`,
`content:category:${event.site}:${event.slug}`,
],
};
export default {
async fetch(request: Request, env: Env) {
const url = new URL(request.url);
if (url.pathname === '/webhooks/perspect' && request.method === 'POST') {
const event = (await request.json()) as WebhookEvent;
// TODO: validate shared secret / signature before proceeding
const buildTags = tagMap[event.type];
if (buildTags) {
const tags = buildTags(event).filter(Boolean);
if (tags.length) {
await perspect.cache.invalidate({ tags });
}
}
return new Response(null, { status: 202 });
}
// ...rest of your app
return new Response('ok');
},
};🔁 Adjust the
WebhookEventunion andtagMapto 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.
## Image Transformations
The SDK includes utilities for transforming images using Cloudflare's Image Resizing service:
```typescript
import { transformMediaItem, buildImageUrl } from 'perspectapi-ts-sdk';
// Get a product with media
const product = await client.products.getProduct('mysite', 123);
const media = product.data.media?.[0];
if (media) {
// Generate all responsive sizes automatically
const urls = transformMediaItem('https://api.perspect.co', media);
console.log(urls.thumbnail); // 150x150 cover crop
console.log(urls.small); // 400px wide
console.log(urls.medium); // 800px wide
console.log(urls.large); // 1200px wide
console.log(urls.original); // Original with auto format
}
// Or build custom transformations
const customUrl = buildImageUrl(
'https://api.perspect.co',
'media/mysite/photo.jpg',
{
width: 400,
height: 300,
fit: 'cover',
format: 'webp',
quality: 85
}
);Features:
- ✅ On-the-fly image resizing (no pre-generated thumbnails)
- ✅ Automatic format optimization (WebP/AVIF)
- ✅ Responsive image support with srcset
- ✅ CDN caching at the edge
- ✅ Bandwidth savings
📚 See docs/image-transforms.md for complete documentation, examples, and best practices.
High-Level Data Loaders
The SDK now ships with convenience loaders that wrap the lower-level REST clients. They help you:
- normalise products (including nested media arrays and Stripe IDs)
- search products server-side with limit/offset support and filter by category names or IDs (no hard-coded IDs)
- fetch published posts/pages with sensible defaults
- fall back to sample data when PerspectAPI is not configured (useful for local previews)
- create Stripe checkout sessions by automatically resolving price IDs
All loaders accept a shared LoaderOptions object:
import {
loadProducts,
loadProductBySlug,
loadPosts,
createCheckoutSession,
type LoaderOptions
} from 'perspectapi-ts-sdk';
const options: LoaderOptions = {
client: createPerspectApiClient({ baseUrl, apiKey }),
siteName: 'museum-indian-art',
logger: console, // optional
fallbackProducts: [], // optional
fallbackPosts: [] // optional
};
const products = await loadProducts(options);
const single = await loadProductBySlug({ ...options, slug: 'sunset-painting' });
const posts = await loadPosts(options);
await createCheckoutSession({
...options,
items: [{ productId: products[0].id, quantity: 1 }],
successUrl: 'https://example.com/success',
cancelUrl: 'https://example.com/cancel'
});
// Filter products by multiple category names/IDs while combining search
const decorPillows = await loadProducts({
...options,
category: ['Home Decor', 'Seasonal Sale'],
categoryIds: [12],
search: 'pillow',
offset: 24,
limit: 12
});More real-world scenarios—including Cloudflare Workers, Remix/Next.js loaders, custom logger implementations, and checkout price resolution—are documented in docs/loaders.md.
Authentication
API Key Authentication
const client = createPerspectApiClient({
baseUrl: 'https://api.example.com',
apiKey: 'pk_your_api_key_here'
});JWT Token Authentication
const client = createPerspectApiClient({
baseUrl: 'https://api.example.com',
jwt: 'your.jwt.token'
});
// Or authenticate after initialization
await client.auth.signIn({
email: 'user@example.com',
password: 'password'
});Dynamic Authentication
const client = createPerspectApiClient({
baseUrl: 'https://api.example.com'
});
// Set authentication later
client.setApiKey('pk_your_api_key');
// or
client.setAuth('your.jwt.token');
// Clear authentication
client.clearAuth();API Reference
Content Management
// Get all content with pagination
const content = await client.content.getContent('your-site-name', {
page: 1,
limit: 20,
page_status: 'publish',
page_type: 'post',
slug_prefix: 'blog' // Optional: filter by slug prefix
});
// Get content by ID
const post = await client.content.getContentById(123);
// Get content by slug
const page = await client.content.getContentBySlug('your-site-name', 'about-us');
// Get content by category slug
const categoryContent = await client.content.getContentByCategorySlug(
'your-site-name',
'news',
{
page: 1,
limit: 20,
page_type: 'post'
}
);
// Create new content
const newPost = await client.content.createContent({
page_title: 'My New Post',
page_content: '<p>Content goes here</p>',
content_markdown: '# My New Post\n\nContent goes here',
page_status: 'draft',
page_type: 'post',
slug: 'my-new-post'
});
// Update content
const updated = await client.content.updateContent(123, {
page_title: 'Updated Title',
page_status: 'publish'
});
// Delete content
await client.content.deleteContent(123);
// Publish/unpublish content
await client.content.publishContent(123);
await client.content.unpublishContent(123);Products & E-commerce
// Get all products
const products = await client.products.getProducts('my-site', {
page: 1,
limit: 20,
isActive: true,
slug_prefix: 'shop' // Optional: filter by slug prefix
});
// Create new product
const product = await client.products.createProduct({
name: 'Amazing Product',
description: 'Product description',
price: 29.99,
currency: 'USD',
sku: 'PROD-001'
});
// Update product inventory
await client.products.updateProductInventory(123, {
quantity: 10,
operation: 'add',
reason: 'Restocked'
});
// Get product by slug and site name (NEW!)
const productBySlug = await client.products.getProductBySlug('my-site', 'awesome-laptop');
console.log('Product:', productBySlug.data?.name);
console.log('Variants:', productBySlug.data?.variants);
// Get products by category slug (NEW!)
const categoryProducts = await client.products.getProductsByCategorySlug(
'my-site',
'electronics',
{
page: 1,
limit: 20,
published: true,
search: 'phone'
}
);
console.log('Products:', categoryProducts.data?.data);
console.log('Category info:', categoryProducts.data?.category);Organizations & Sites
// Get organizations
const orgs = await client.organizations.getOrganizations();
// Create new organization
const org = await client.organizations.createOrganization({
name: 'My Company',
description: 'Company description'
});
// Get sites
const sites = await client.sites.getSites({
organizationId: 1
});
// Create new site
const site = await client.sites.createSite({
name: 'My Website',
domain: 'example.com',
organizationId: 1
});API Key Management
// Get all API keys
const apiKeys = await client.apiKeys.getApiKeys();
// Create new API key
const newKey = await client.apiKeys.createApiKey({
name: 'Frontend App Key',
description: 'API key for frontend application',
permissions: ['content:read', 'products:read'],
expiresAt: '2024-12-31T23:59:59Z'
});
console.log('New API key:', newKey.data.key);
// Test API key validity
const testResult = await client.apiKeys.testApiKey('pk_test_key');
console.log('Key valid:', testResult.data.valid);Webhooks
// Get all webhooks
const webhooks = await client.webhooks.getWebhooks();
// Create new webhook
const webhook = await client.webhooks.createWebhook({
name: 'Order Notifications',
url: 'https://myapp.com/webhooks/orders',
provider: 'stripe',
events: ['payment_intent.succeeded', 'payment_intent.payment_failed'],
isActive: true
});
// Test webhook
const testResult = await client.webhooks.testWebhook(webhook.data.id);
console.log('Webhook test:', testResult.data);Checkout & Payments
// Create Stripe checkout session
const session = await client.checkout.createCheckoutSession({
priceId: 'price_1234567890',
successUrl: 'https://myapp.com/success',
cancelUrl: 'https://myapp.com/cancel',
customerEmail: 'customer@example.com',
currency: 'usd',
shipping_amount: 500,
shipping_address: {
country: 'US',
state: 'CA',
postal_code: '94110'
},
billing_address: {
country: 'US',
state: 'CA',
postal_code: '94110'
},
tax: {
strategy: 'manual_rates',
customer_identifier: 'acct-123',
customer_display_name: 'Acme Corp',
customer_exemption: {
status: 'exempt',
tax_id: '99-1234567',
tax_id_type: 'ein'
}
}
});
// Redirect user to checkout
window.location.href = session.data.url;
// Get checkout session status
const sessionStatus = await client.checkout.getCheckoutSession('cs_test_123');
console.log(sessionStatus.data.tax?.amount); // Display assessed tax (if calculated)Contact Forms
const siteName = 'your-site-name';
// Submit contact form
const submission = await client.contact.submitContact(siteName, {
name: 'John Doe',
email: 'john@example.com',
subject: 'Question about pricing',
message: 'I have a question about your pricing plans.',
turnstileToken: 'turnstile-response-token'
});
// Get contact submissions (admin only)
const submissions = await client.contact.getContactSubmissions(siteName, {
status: 'unread',
page: 1,
limit: 50
});
// Update submission status
await client.contact.updateContactStatus(siteName, submission.data.id, 'read');Newsletter Subscriptions
const siteName = 'your-site-name';
// Subscribe to newsletter (with double opt-in)
const subscription = await client.newsletter.subscribe(siteName, {
email: 'subscriber@example.com',
name: 'Jane Doe',
list_ids: ['list_default'], // Optional: subscribe to specific lists
frequency: 'weekly', // instant, daily, weekly, monthly
topics: ['news', 'updates'], // Optional: topic preferences
double_opt_in: true, // Default: true for GDPR compliance
turnstile_token: 'token' // Optional: Turnstile verification
});
// Confirm subscription via email token
const confirmed = await client.newsletter.confirmSubscription(
siteName,
'confirmation-token-from-email'
);
// Unsubscribe
const unsubscribed = await client.newsletter.unsubscribe(siteName, {
email: 'subscriber@example.com',
reason: 'Too many emails' // Optional feedback
});
// One-click unsubscribe (from email link)
await client.newsletter.unsubscribeByToken(siteName, 'unsubscribe-token');
// Update preferences
await client.newsletter.updatePreferences(siteName, 'subscriber@example.com', {
frequency: 'monthly',
topics: ['product-updates'],
email_format: 'html', // html, text, or both
timezone: 'America/New_York',
track_opens: false,
track_clicks: false
});
// Check subscription status
const status = await client.newsletter.getStatus(siteName, 'subscriber@example.com');
console.log('Subscribed:', status.data.subscribed);
console.log('Status:', status.data.status); // pending, confirmed, unsubscribed
// Get available newsletter lists
const lists = await client.newsletter.getLists(siteName);
console.log('Available lists:', lists.data.lists);Newsletter Admin Functions
// Get all subscriptions (admin only)
const subscriptions = await client.newsletter.getSubscriptions(siteName, {
page: 1,
limit: 100,
status: 'confirmed',
list_id: 'list_weekly',
search: 'john',
startDate: '2024-01-01',
endDate: '2024-12-31'
});
// Update subscription status
await client.newsletter.updateSubscriptionStatus(
siteName,
'sub_123',
'unsubscribed',
'Admin action: User requested via support'
);
// Bulk operations
await client.newsletter.bulkUpdateSubscriptions(siteName, {
ids: ['sub_123', 'sub_456'],
action: 'add_to_list',
list_id: 'list_special_offers'
});
// List management
const newList = await client.newsletter.createList(siteName, {
list_name: 'VIP Customers',
slug: 'vip-customers',
description: 'Exclusive updates for VIP customers',
is_public: false,
is_default: false,
double_opt_in: true,
welcome_email_enabled: true
});
await client.newsletter.updateList(siteName, 'list_123', {
description: 'Updated description',
is_public: true
});
// Get statistics
const stats = await client.newsletter.getStatistics(siteName, {
startDate: '2024-01-01',
endDate: '2024-12-31',
list_id: 'list_weekly'
});
console.log('Total subscribers:', stats.data.totalSubscribers);
console.log('Open rate:', stats.data.engagementMetrics.averageOpenRate);
// Export subscriptions
const exportData = await client.newsletter.exportSubscriptions(siteName, {
format: 'csv', // csv, json, or xlsx
status: 'confirmed',
list_id: 'list_weekly'
});
console.log('Download URL:', exportData.data.downloadUrl);
// Import subscriptions
const importResult = await client.newsletter.importSubscriptions(siteName, {
subscriptions: [
{ email: 'user1@example.com', name: 'User One', lists: ['list_weekly'] },
{ email: 'user2@example.com', name: 'User Two', lists: ['list_daily'] }
],
skip_confirmation: false, // Skip double opt-in for imported users
update_existing: true // Update if email already exists
});
console.log('Imported:', importResult.data.imported);
console.log('Failed:', importResult.data.failed);📚 For complete newsletter documentation, see docs/newsletter.md
Site Users (Customer Accounts)
Site users are per-site customer accounts with OTP-based authentication, separate from admin users.
const siteName = 'your-site-name';
// Request OTP for login/signup (with optional metadata)
await client.siteUsers.requestOtp(
siteName,
{
email: 'user@example.com',
waitlist: true, // Optional: mark as waitlist signup
metadata: { // Optional: set metadata at signup time
signupSource: 'landing-page',
referralCode: 'FRIEND123',
interests: ['product-updates']
}
},
csrfToken
);
// Verify OTP and get JWT token
const response = await client.siteUsers.verifyOtp(
siteName,
{ email: 'user@example.com', code: '123456' },
csrfToken
);
const { token, user } = response.data;
client.setAuth(token); // Set auth for subsequent requests
// Get current user profile
const { data } = await client.siteUsers.getMe(siteName);
console.log(data.user); // Basic user fields + metadata
console.log(data.profile); // Key-value profile data
// Update user profile and metadata
await client.siteUsers.updateMe(
siteName,
{
first_name: 'John',
last_name: 'Doe',
metadata: {
preferences: { theme: 'dark' },
tags: ['premium'],
customField: 'value'
}
},
csrfToken
);
// Set profile key-values (phone, addresses, etc.)
await client.siteUsers.setProfileValue(
siteName,
'phone',
'+1-555-0123',
csrfToken
);
await client.siteUsers.setProfileValue(
siteName,
'address_shipping',
JSON.stringify({
line1: '123 Main St',
city: 'San Francisco',
state: 'CA',
postal_code: '94102',
country: 'US'
}),
csrfToken
);
// Get order history
const orders = await client.siteUsers.getOrders(siteName, {
limit: 50,
offset: 0
});
// Get subscriptions
const subscriptions = await client.siteUsers.getSubscriptions(siteName);
// Cancel subscription
await client.siteUsers.cancelSubscription(siteName, 'sub_123', csrfToken);
// Logout
await client.siteUsers.logout(siteName);
client.setAuth(null);📚 For complete site users documentation including waitlist management, metadata patterns, cross-domain authentication, and complete examples, see docs/site-users.md
Configuration Options
interface PerspectApiConfig {
baseUrl: string; // Required: API base URL
apiKey?: string; // Optional: API key for authentication
jwt?: string; // Optional: JWT token for authentication
timeout?: number; // Optional: Request timeout in ms (default: 30000)
retries?: number; // Optional: Number of retries (default: 3)
headers?: Record<string, string>; // Optional: Additional headers
}Error Handling
The SDK provides structured error handling:
import { createApiError } from 'perspectapi-ts-sdk';
try {
const content = await client.content.getContentById(999);
} catch (error) {
const apiError = createApiError(error);
console.log('Error message:', apiError.message);
console.log('Status code:', apiError.status);
console.log('Error code:', apiError.code);
console.log('Details:', apiError.details);
}Cloudflare Workers Usage
The SDK is fully compatible with Cloudflare Workers:
// worker.ts
import { createPerspectApiClient } from 'perspectapi-ts-sdk';
export default {
async fetch(request: Request, env: any): Promise<Response> {
const client = createPerspectApiClient({
baseUrl: env.PERSPECT_API_URL,
apiKey: env.PERSPECT_API_KEY
});
try {
const content = await client.content.getContent(env.PERSPECT_SITE_NAME, { limit: 10 });
return new Response(JSON.stringify(content.data), {
headers: { 'Content-Type': 'application/json' }
});
} catch (error) {
return new Response('Error fetching content', { status: 500 });
}
}
};Advanced Usage
Using Individual Clients
import { HttpClient, ContentClient } from 'perspectapi-ts-sdk';
// Create HTTP client
const http = new HttpClient({
baseUrl: 'https://api.example.com',
apiKey: 'your-api-key'
});
// Use individual client
const contentClient = new ContentClient(http);
const content = await contentClient.getContent('your-site-name');Custom Headers
const client = createPerspectApiClient({
baseUrl: 'https://api.example.com',
apiKey: 'your-api-key',
headers: {
'X-Custom-Header': 'custom-value',
'User-Agent': 'MyApp/1.0.0'
}
});Timeout and Retries
const client = createPerspectApiClient({
baseUrl: 'https://api.example.com',
apiKey: 'your-api-key',
timeout: 60000, // 60 seconds
retries: 5 // Retry up to 5 times
});Slug Prefix Filtering
The SDK supports filtering content and products by slug_prefix to organize your content by URL structure:
// Filter blog posts
const blogPosts = await client.content.getContent('mysite', {
slug_prefix: 'blog', // /blog/post-1, /blog/post-2
page_status: 'publish'
});
// Filter news articles
const news = await client.content.getContent('mysite', {
slug_prefix: 'news', // /news/article-1, /news/article-2
page_status: 'publish'
});
// Filter shop products
const shopProducts = await client.products.getProducts('mysite', {
slug_prefix: 'shop', // /shop/product-1, /shop/product-2
isActive: true
});
// Filter artwork products
const artwork = await client.products.getProducts('mysite', {
slug_prefix: 'artwork', // /artwork/painting-1, /artwork/sculpture-1
isActive: true
});
// Combine with other filters
const results = await client.content.getContent('mysite', {
slug_prefix: 'blog',
page_status: 'publish',
page_type: 'post',
search: 'javascript', // Search within blog posts only
page: 1,
limit: 20
});Use Cases:
- Organize blog posts, news, and documentation separately
- Separate shop products from artwork or digital downloads
- Create multi-section websites with distinct URL structures
- Filter content/products by section for navigation
See examples/slug-prefix-examples.ts for 20+ real-world examples!
TypeScript Support
The SDK is written in TypeScript and provides comprehensive type definitions:
import type {
Content,
Product,
ApiResponse,
PaginatedResponse
} from 'perspectapi-ts-sdk';
// Fully typed responses
const content: ApiResponse<Content> = await client.content.getContentById(123);
const products: PaginatedResponse<Product> = await client.products.getProducts('your-site-name');Contributing
- Fork the repository
- Create your feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add some amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
License
This project is licensed under the MIT License - see the LICENSE file for details.